pxcbackup 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 048fb039a8728b1281f0e6525e67570b4c218f51
4
- data.tar.gz: d93238bcc021b538bf75c8551fc79e9631b6d976
3
+ metadata.gz: 95db3e154a55db8fe7bd7ea736d2f197a51a7f4f
4
+ data.tar.gz: 0c8da2018cecf4ee57bb5eb54ed28972a84d713f
5
5
  SHA512:
6
- metadata.gz: 60ee86b267cfa743ab4ecf8bc7b792272a6fe3fbed583697cffdcc55be51087bceada6d92462342103c49fa3c11769cde53c4e72c3361215ed958b1681765f24
7
- data.tar.gz: c88134fb4dc0ad0345bb860d1ccdb10bd17cfbd65e2bbe7556fc6ac68e0d4bbfdc80189cf96b857821fba02468d84faacadedbfc79380f2ecefb83088fecb1ec
6
+ metadata.gz: 2430921b6608a2d59aad06d6ffae4e538a6ba5bceabbe125ae8d8880c929ca4fee29f008de84a4b272cbc935c4f99e5ba0d8e4dc6d004acb99de886e3d7f60bd
7
+ data.tar.gz: efd83314d29a5c9cb5c21f86fdc0ff33158cc8b8f068f173e31412fc099e0e584fe7deb1f51f563656f901d3392436ecd71a8a7e0ad17c19e63f2368d5b2a3ce
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # PXCBackup
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/pxcbackup.svg)](http://badge.fury.io/rb/pxcbackup)
4
+
3
5
  PXCBackup is a database backup tool meant for [Percona XtraDB Cluster](http://www.percona.com/software/percona-xtradb-cluster) (PXC), although it could also be used on other related systems, like a [MariaDB](https://mariadb.org) [Galera](http://galeracluster.com/products/) cluster using [XtraBackup](http://www.percona.com/software/percona-xtrabackup), for example.
4
6
 
5
7
  The `innobackupex` script provided by Percona makes it very easy to create backups, however restoring backups can become quite complicated, since backups might need to be extracted, uncompressed, decrypted, before restoring they need to be prepared, incremental backups need to be applied on top of full backups, indexes might need to be rebuilt for compact backups, etc. Usually, backups need to be restored in stressful emergency situations, where all of these steps can slow you down quite a bit.
@@ -2,6 +2,10 @@ require 'optparse'
2
2
  require 'time'
3
3
  require 'yaml'
4
4
 
5
+ require 'pxcbackup/backupper'
6
+ require 'pxcbackup/logger'
7
+ require 'pxcbackup/version'
8
+
5
9
  module PXCBackup
6
10
  class Application
7
11
  def initialize(argv)
@@ -20,6 +24,7 @@ module PXCBackup
20
24
  end
21
25
 
22
26
  def run
27
+ Logger.color_output = ENV['TERM'] && !@options[:no_color]
23
28
  backupper = Backupper.new(@options)
24
29
 
25
30
  case @command
@@ -46,6 +51,10 @@ module PXCBackup
46
51
  opt.separator ''
47
52
  opt.separator 'Options'
48
53
 
54
+ opt.on('--no-color', 'disable color output') do |color_output|
55
+ @options[:no_color] = true
56
+ end
57
+
49
58
  opt.on('-c', '--config', '=CONFIG_FILE', 'config file to use instead of ~/.pxcbackup') do |config_file|
50
59
  @options[:config] = config_file
51
60
  end
@@ -71,7 +80,12 @@ module PXCBackup
71
80
  end
72
81
 
73
82
  opt.on('-v', '--verbose', 'verbose output') do
74
- @options[:verbose] = true
83
+ Logger.raise_verbosity
84
+ end
85
+
86
+ opt.on('--version', 'print version and exit') do
87
+ puts "pxcbackup #{VERSION}"
88
+ exit
75
89
  end
76
90
 
77
91
  opt.on('-y', '--yes', 'skip confirmation on backup restore') do
@@ -23,7 +23,7 @@ module PXCBackup
23
23
  end
24
24
 
25
25
  def to_s
26
- time.to_s
26
+ "#{time} - #{type.to_s[0..3]} (#{remote? ? 'remote' : 'local'})"
27
27
  end
28
28
 
29
29
  def time
@@ -1,9 +1,10 @@
1
1
  require 'fileutils'
2
- require 'open3'
3
2
  require 'tmpdir'
4
3
 
5
4
  require 'pxcbackup/array'
6
5
  require 'pxcbackup/backup'
6
+ require 'pxcbackup/command'
7
+ require 'pxcbackup/logger'
7
8
  require 'pxcbackup/mysql'
8
9
  require 'pxcbackup/path_resolver'
9
10
  require 'pxcbackup/remote_repo'
@@ -12,9 +13,10 @@ require 'pxcbackup/repo'
12
13
  module PXCBackup
13
14
  class Backupper
14
15
  def initialize(options)
15
- @verbose = options[:verbose] || false
16
16
  @threads = options[:threads] || 1
17
17
  @memory = options[:memory] || '100M'
18
+
19
+ @defaults_file = options[:defaults_file] || nil
18
20
  @throttle = options[:throttle] || nil
19
21
  @encrypt = options[:encrypt] || nil
20
22
  @encrypt_key = options[:encrypt_key] || nil
@@ -78,7 +80,7 @@ module PXCBackup
78
80
 
79
81
  Dir.mktmpdir('pxcbackup-') do |dir|
80
82
  arguments << dir.shellescape
81
- log_action "Creating backup #{filename}" do
83
+ Logger.action "Creating backup #{filename}" do
82
84
  innobackupex(arguments, File.join(@local_repo.path, filename))
83
85
  end
84
86
  end
@@ -86,7 +88,11 @@ module PXCBackup
86
88
  desync_disable
87
89
  rotate(retention)
88
90
 
89
- @remote_repo.sync(@local_repo) if @remote_repo
91
+ if @remote_repo
92
+ Logger.action 'Syncing backups to remote repository' do
93
+ @remote_repo.sync(@local_repo)
94
+ end
95
+ end
90
96
  end
91
97
 
92
98
  def restore_backup(time, skip_confirmation = false)
@@ -101,7 +107,8 @@ module PXCBackup
101
107
 
102
108
  full_backup = incremental_backups.shift
103
109
 
104
- log "[1/#{incremental_backups.size + 1}] Processing #{full_backup.type.to_s} backup from #{full_backup}"
110
+ Logger.info "[1/#{incremental_backups.size + 1}] Processing #{full_backup.type.to_s} backup from #{full_backup.time}"
111
+ Logger.increase_indentation
105
112
  with_extracted_backup(full_backup) do |full_backup_path, full_backup_info|
106
113
  raise 'unexpected backup type' unless full_backup_info[:backup_type] == full_backup.type
107
114
  raise 'unexpected start LSN' unless full_backup_info[:from_lsn] == 0
@@ -109,22 +116,23 @@ module PXCBackup
109
116
  compact = full_backup_info[:compact]
110
117
 
111
118
  if full_backup_info[:compress]
112
- log_action ' Decompressing' do
119
+ Logger.action 'Decompressing' do
113
120
  innobackupex(['--decompress', full_backup_path.shellescape])
114
121
  end
115
122
  end
116
123
 
117
124
  if incremental_backups.any?
118
- log_action " Preparing base backup (LSN #{full_backup_info[:to_lsn]})" do
125
+ Logger.action "Preparing base backup (LSN #{full_backup_info[:to_lsn]})" do
119
126
  innobackupex(['--apply-log', '--redo-only', full_backup_path.shellescape])
120
127
  end
121
128
 
122
129
  current_lsn = full_backup_info[:to_lsn]
130
+ Logger.decrease_indentation
123
131
 
124
132
  index = 2
125
133
  incremental_backups.each do |incremental_backup|
126
- log "[#{index}/#{incremental_backups.size + 1}] Processing #{incremental_backup.type.to_s} backup from #{incremental_backup}"
127
- index += 1
134
+ Logger.info "[#{index}/#{incremental_backups.size + 1}] Processing #{incremental_backup.type.to_s} backup from #{incremental_backup.time}"
135
+ Logger.increase_indentation
128
136
  with_extracted_backup(incremental_backup) do |incremental_backup_path, incremental_backup_info|
129
137
  raise 'unexpected backup type' unless incremental_backup_info[:backup_type] == incremental_backup.type
130
138
  raise 'unexpected start LSN' unless incremental_backup_info[:from_lsn] == current_lsn
@@ -132,17 +140,19 @@ module PXCBackup
132
140
  compact ||= incremental_backup_info[:compact]
133
141
 
134
142
  if incremental_backup_info[:compress]
135
- log_action ' Decompressing' do
143
+ Logger.action 'Decompressing' do
136
144
  innobackupex(['--decompress', incremental_backup_path.shellescape])
137
145
  end
138
146
  end
139
147
 
140
- log_action " Applying increment (LSN #{incremental_backup_info[:from_lsn]} -> #{incremental_backup_info[:to_lsn]})" do
148
+ Logger.action "Applying increment (LSN #{incremental_backup_info[:from_lsn]} -> #{incremental_backup_info[:to_lsn]})" do
141
149
  innobackupex(['--apply-log', '--redo-only', full_backup_path.shellescape, "--incremental-dir=#{incremental_backup_path.shellescape}"])
142
150
  end
143
151
 
144
152
  current_lsn = incremental_backup_info[:to_lsn]
153
+ Logger.decrease_indentation
145
154
  end
155
+ index += 1
146
156
  end
147
157
  end
148
158
 
@@ -156,12 +166,12 @@ module PXCBackup
156
166
  arguments << '--rebuild-indexes'
157
167
  end
158
168
 
159
- log_action "#{action}" do
169
+ Logger.action "#{action}" do
160
170
  arguments << full_backup_path.shellescape
161
171
  innobackupex(arguments)
162
172
  end
163
173
 
164
- log_action 'Attempting to restore Galera info' do
174
+ Logger.action 'Attempting to restore Galera info' do
165
175
  restore_galera_info(full_backup_path)
166
176
  end
167
177
 
@@ -194,8 +204,8 @@ module PXCBackup
194
204
  raise 'did not confirm restore' unless confirmation == 'yes'
195
205
  end
196
206
 
197
- log_action 'Stopping MySQL server' do
198
- system("#{@which.service.shellescape} mysql stop")
207
+ Logger.action 'Stopping MySQL server' do
208
+ Command.run("#{@which.service.shellescape} mysql stop")
199
209
  end
200
210
 
201
211
  stat = File.stat(mysql_datadir)
@@ -203,39 +213,33 @@ module PXCBackup
203
213
  gid = stat.gid
204
214
 
205
215
  mysql_datadir_old = mysql_datadir + '_' + Time.now.strftime('%Y%m%d%H%M%S')
206
- log_action "Moving current datadir to #{mysql_datadir_old}" do
216
+ Logger.action "Moving current datadir to #{mysql_datadir_old}" do
207
217
  File.rename(mysql_datadir, mysql_datadir_old)
208
218
  end
209
219
 
210
- log_action "Restoring backup to #{mysql_datadir}" do
220
+ Logger.action "Restoring backup to #{mysql_datadir}" do
211
221
  Dir.mkdir(mysql_datadir)
212
222
  innobackupex(['--move-back', full_backup_path.shellescape])
213
223
  end
214
224
 
215
- log_action "Chowning #{mysql_datadir}" do
225
+ Logger.action "Chowning #{mysql_datadir}" do
216
226
  FileUtils.chown_R(uid, gid, mysql_datadir)
217
227
  end
218
228
 
219
229
  if @local_repo
220
- log_action "Removing last backup info" do
230
+ Logger.action "Removing last backup info" do
221
231
  File.delete(File.join(@local_repo.path, 'xtrabackup_checkpoints'))
222
232
  end
223
233
  end
224
234
 
225
- log_action 'Starting MySQL server' do
226
- system("#{@which.service.shellescape} mysql start")
235
+ Logger.action 'Starting MySQL server' do
236
+ Command.run("#{@which.service.shellescape} mysql start")
227
237
  end
228
238
  end
229
239
  end
230
240
 
231
241
  def list_backups
232
- all_backups.each do |backup|
233
- if @verbose
234
- puts "#{backup} - #{backup.type.to_s[0..3]} (#{backup.remote? ? 'remote' : 'local'})"
235
- else
236
- puts backup
237
- end
238
- end
242
+ all_backups.each { |backup| puts backup }
239
243
  end
240
244
 
241
245
  private
@@ -248,61 +252,38 @@ module PXCBackup
248
252
  backups.sort
249
253
  end
250
254
 
251
- def log(text)
252
- return unless @verbose
253
- previous_stdout = $stdout
254
- $stdout = STDOUT
255
- puts text if @verbose
256
- $stdout = previous_stdout
257
- end
258
-
259
- def log_action(text)
260
- return yield unless @verbose
261
-
262
- begin
263
- print "#{text}... "
264
- previous_stdout, previous_stderr = $stdout, $stderr
265
- begin
266
- $stdout = $stderr = File.new('/dev/null', 'w')
267
- t1 = Time.now
268
- yield
269
- t2 = Time.now
270
- ensure
271
- $stdout, $stderr = previous_stdout, previous_stderr
272
- end
273
- rescue => e
274
- puts "fail"
275
- raise e
276
- else
277
- puts "done (%.1fs)" % (t2 - t1)
278
- end
279
- end
280
-
281
255
  def desync_enable(wait = 60)
282
- log "Setting wsrep_desync=ON and waiting for #{wait} seconds"
256
+ Logger.info 'Setting wsrep_desync=ON'
283
257
  @mysql.set_variable('wsrep_desync', 'ON')
284
- sleep(wait)
258
+ Logger.action "Waiting for #{wait} seconds" do
259
+ sleep(wait)
260
+ end
285
261
  end
286
262
 
287
263
  def desync_disable
288
- log 'Waiting until wsrep_local_recv_queue is empty'
289
- sleep(2) until @mysql.get_status('wsrep_local_recv_queue') == '0'
290
- log 'Setting wsrep_desync=OFF'
264
+ Logger.action 'Waiting until wsrep_local_recv_queue is empty' do
265
+ sleep(2) until @mysql.get_status('wsrep_local_recv_queue') == '0'
266
+ end
267
+ Logger.info 'Setting wsrep_desync=OFF'
291
268
  @mysql.set_variable('wsrep_desync', 'OFF')
292
269
  end
293
270
 
294
271
  def rotate(retention)
295
- log 'Checking if we have old backups to remove'
296
- @local_repo.backups.each do |backup|
297
- days = (Time.now - backup.time) / 86400
298
- break if days < retention && backup.full?
299
- log "Deleting backup #{backup}"
300
- backup.delete
272
+ Logger.action 'Checking if we have old backups to remove' do
273
+ @local_repo.backups.each do |backup|
274
+ days = (Time.now - backup.time) / 86400
275
+ break if days < retention && backup.full?
276
+ Logger.info "Deleting backup from #{backup.time}"
277
+ backup.delete
278
+ end
301
279
  end
302
280
  end
303
281
 
304
282
  def innobackupex(arguments, output_file = nil)
305
283
  command = @which.innobackupex.shellescape
284
+ # --defaults-file has to be the first option passed!
285
+ command << " --defaults-file=#{@defaults_file.shellescape}" if @defaults_file
286
+
306
287
  arguments += [
307
288
  "--ibbackup=#{@which.xtrabackup.shellescape}",
308
289
  "--parallel=#{@threads}",
@@ -315,11 +296,8 @@ module PXCBackup
315
296
 
316
297
  command << ' ' + arguments.join(' ')
317
298
  command << " > #{output_file.shellescape}" if output_file
318
- log = Open3.popen3(command) do |stdin, stdout, stderr|
319
- stderr.read
320
- end
321
- exit_status = $?
322
- raise 'something went wrong with innobackupex' unless exit_status.success? && log.lines.to_a.last.match(/: completed OK!$/)
299
+ result = Command.run(command)
300
+ raise 'unexpected output from innobackupex' unless result[:stderr].lines.to_a.last.match(/: completed OK!$/)
323
301
  end
324
302
 
325
303
  def read_backup_info(file)
@@ -359,8 +337,8 @@ module PXCBackup
359
337
  when :tar
360
338
  " | #{@which.tar.shellescape} -ixf - -C #{dir.shellescape}"
361
339
  end
362
- log_action " #{action}" do
363
- system(command)
340
+ Logger.action action do
341
+ Command.run(command)
364
342
  end
365
343
 
366
344
  info = read_backup_info(File.join(dir, 'xtrabackup_checkpoints'))
@@ -0,0 +1,32 @@
1
+ require 'open3'
2
+
3
+ require 'pxcbackup/logger'
4
+
5
+ module PXCBackup
6
+ module Command
7
+ def self.run(command, ignore_exit_status = false)
8
+ Logger.debug "# #{command}"
9
+ captured_stdout = ''
10
+ captured_stderr = ''
11
+ Open3.popen3(command) do |stdin, stdout, stderr|
12
+ stdin.close
13
+ until stdout.closed? && stderr.closed?
14
+ sockets = []
15
+ sockets << stdout unless stdout.closed?
16
+ sockets << stderr unless stderr.closed?
17
+ IO.select(sockets).flatten.compact.each do |socket|
18
+ begin
19
+ data = socket.readpartial(1024)
20
+ captured_stdout << data if socket == stdout
21
+ captured_stderr << data if socket == stderr
22
+ rescue EOFError
23
+ socket.close
24
+ end
25
+ end
26
+ end
27
+ end
28
+ raise 'command "#{command.split.first}" exited with a non-zero status' unless $?.success? || ignore_exception
29
+ { :stdout => captured_stdout, :stderr => captured_stderr, :exit_status => $?.exitstatus }
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,96 @@
1
+ module PXCBackup
2
+ module Logger
3
+ @verbosity_level = 0
4
+ @indentation = 0
5
+ @color_output = false
6
+ @partial = false
7
+
8
+ def self.raise_verbosity
9
+ @verbosity_level += 1
10
+ end
11
+
12
+ def self.increase_indentation
13
+ @indentation += 1
14
+ end
15
+
16
+ def self.decrease_indentation
17
+ @indentation -= 1
18
+ end
19
+
20
+ def self.color_output=(value)
21
+ @color_output = value
22
+ end
23
+
24
+ def self.output(message, skip_newline = false)
25
+ if @partial
26
+ puts
27
+ increase_indentation
28
+ @partial = false
29
+ end
30
+ print ' ' * @indentation + message
31
+ puts unless skip_newline
32
+ $stdout.flush
33
+ end
34
+
35
+ def self.action_start(message)
36
+ return unless @verbosity_level >= 1
37
+ output "#{message}: ", true
38
+ @partial = true
39
+ end
40
+
41
+ def self.action_end(message)
42
+ return unless @verbosity_level >= 1
43
+ if @partial
44
+ puts message
45
+ @partial = false
46
+ else
47
+ output message
48
+ decrease_indentation
49
+ end
50
+ end
51
+
52
+ def self.info(message)
53
+ output message if @verbosity_level >= 1
54
+ end
55
+
56
+ def self.debug(message)
57
+ output blue(message) if @verbosity_level >= 2
58
+ end
59
+
60
+ def self.action(message)
61
+ return yield unless @verbosity_level >= 1
62
+
63
+ action_start(message)
64
+ t1 = Time.now
65
+ begin
66
+ result = yield
67
+ rescue => e
68
+ action_end(red('fail'))
69
+ raise e
70
+ end
71
+ t2 = Time.now
72
+ action_end(green('done') + ' (%.1fs)' % (t2 - t1))
73
+ result
74
+ end
75
+
76
+ def self.colorize(text, color_code)
77
+ @color_output ? "\e[#{color_code}m#{text}\e[0m" : text
78
+ end
79
+
80
+ def self.red(text)
81
+ colorize(text, 31);
82
+ end
83
+
84
+ def self.green(text)
85
+ colorize(text, 32)
86
+ end
87
+
88
+ def self.yellow(text)
89
+ colorize(text, 33);
90
+ end
91
+
92
+ def self.blue(text)
93
+ colorize(text, 34);
94
+ end
95
+ end
96
+ end
@@ -1,5 +1,7 @@
1
1
  require 'shellwords'
2
2
 
3
+ require 'pxcbackup/command'
4
+
3
5
  module PXCBackup
4
6
  class MySQL
5
7
  attr_reader :datadir
@@ -17,7 +19,8 @@ module PXCBackup
17
19
  end
18
20
 
19
21
  def exec(query)
20
- lines = `echo #{query.shellescape} | #{@which.mysql.shellescape} #{auth} 2> /dev/null`.lines.to_a
22
+ output = Command.run("echo #{query.shellescape} | #{@which.mysql.shellescape} #{auth}", true)
23
+ lines = output[:stdout].lines.to_a
21
24
  return nil if lines.empty?
22
25
 
23
26
  keys = lines.shift.chomp.split("\t")
@@ -1,7 +1,8 @@
1
1
  require 'shellwords'
2
2
 
3
- require 'pxcbackup/backup'
4
- require 'pxcbackup/repo'
3
+ require 'pxcbackup/backup'
4
+ require 'pxcbackup/command'
5
+ require 'pxcbackup/repo'
5
6
 
6
7
  module PXCBackup
7
8
  class RemoteRepo < Repo
@@ -12,7 +13,8 @@ module PXCBackup
12
13
 
13
14
  def backups
14
15
  backups = []
15
- `#{@which.s3cmd.shellescape} ls #{@path.shellescape}`.lines.to_a.each do |line|
16
+ output = Command.run("#{@which.s3cmd.shellescape} ls #{@path.shellescape}")
17
+ output[:stdout].lines.to_a.each do |line|
16
18
  path = line.chomp.split[3]
17
19
  next unless Backup.regexp.match(path)
18
20
  backups << Backup.new(self, path)
@@ -23,12 +25,12 @@ module PXCBackup
23
25
  def sync(local_repo)
24
26
  source = File.join(local_repo.path, '/')
25
27
  target = File.join(path, '/')
26
- system("#{@which.s3cmd.shellescape} sync --no-progress --delete-removed #{source.shellescape} #{target.shellescape} > /dev/null")
28
+ Command.run("#{@which.s3cmd.shellescape} sync --no-progress --delete-removed #{source.shellescape} #{target.shellescape}")
27
29
  end
28
30
 
29
31
  def delete(backup)
30
32
  verify(backup)
31
- system("#{@which.s3cmd.shellescape} del #{backup.path.shellescape} > /dev/null")
33
+ Command.run("#{@which.s3cmd.shellescape} del #{backup.path.shellescape}")
32
34
  end
33
35
 
34
36
  def stream_command(backup)
@@ -24,7 +24,7 @@ module PXCBackup
24
24
 
25
25
  def delete(backup)
26
26
  verify(backup)
27
- system("#{@which.rm.shellescape} #{backup.path.shellescape}")
27
+ File.delete(backup.path)
28
28
  end
29
29
 
30
30
  def stream_command(backup)
@@ -1,3 +1,3 @@
1
1
  module PXCBackup
2
- VERSION = "0.0.2"
2
+ VERSION = "0.1.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pxcbackup
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robbert Klarenbeek
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-05-08 00:00:00.000000000 Z
11
+ date: 2014-05-10 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Backup tool for Percona XtraDB Cluster
14
14
  email: robbertkl@renbeek.nl
@@ -27,6 +27,8 @@ files:
27
27
  - lib/pxcbackup/array.rb
28
28
  - lib/pxcbackup/backup.rb
29
29
  - lib/pxcbackup/backupper.rb
30
+ - lib/pxcbackup/command.rb
31
+ - lib/pxcbackup/logger.rb
30
32
  - lib/pxcbackup/mysql.rb
31
33
  - lib/pxcbackup/path_resolver.rb
32
34
  - lib/pxcbackup/remote_repo.rb