lnbackup 2.1

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.
@@ -0,0 +1,1938 @@
1
+ module LnBackup
2
+ CONFIG_D = '/etc/lnbackup.d/'
3
+ LOG_FILE = '/var/log/lnbackup'
4
+ STATUS_FILE_PREF = '/var/log/lnbackup.status'
5
+ PCS_STATUS = '/var/log/lnbackup-pcs.status'
6
+ LILO_PATH = '/sbin/lilo'
7
+
8
+ #### return codes ####
9
+ BACKUP_OK = 0 # 0 -- ok
10
+ BACKUP_EXISTS = 1 # 1 -- warning
11
+ FAILED_TO_FREE_SPACE = 12 # 12... -- error codes
12
+ NO_BACKUP_DIR = 13
13
+ MOUNT_FAILED = 14
14
+ INVALID_BACKUP = 15
15
+ PRE_COMMAND_FAILED = 16
16
+ POST_COMMAND_FAILED = 17
17
+ FSCK_FAILED = 18
18
+ SMBMOUNT_FAILED = 19
19
+ LNBACKUP_RUNNING = 20
20
+ DEVICE_NOT_FOUND = 21
21
+ PIDFILE_FAILED = 22
22
+ DEVICE_FULL = 100 # when running in --no-delete mode and we run out of disk space
23
+
24
+ MESSAGES = {
25
+ BACKUP_EXISTS => 'backup already exists',
26
+ FAILED_TO_FREE_SPACE => 'failed to free space',
27
+ NO_BACKUP_DIR => 'no backup dir',
28
+ MOUNT_FAILED => 'mount failed',
29
+ INVALID_BACKUP => 'invalid backup name',
30
+ PRE_COMMAND_FAILED => 'pre backup cmd failed',
31
+ POST_COMMAND_FAILED => 'post backup cmd failed',
32
+ FSCK_FAILED => 'fsck failed',
33
+ LNBACKUP_RUNNING => 'lnbackup already running',
34
+ DEVICE_NOT_FOUND => 'backup device not found',
35
+ DEVICE_FULL => 'device full',
36
+ PIDFILE_FAILED => 'failed to create pid/lock file'
37
+ }
38
+
39
+ class LnBackup
40
+
41
+ attr :stats
42
+
43
+ def conf_val(key)
44
+ raise "no @cur_config!" unless @cur_config
45
+ @cur_config[key] || @config[key]
46
+ end
47
+
48
+ #### inicializace a konfigurace ####
49
+ def load_config
50
+ @config = {}
51
+ p "config dir: #{@config_dir}"
52
+ @log.debug { "config dir: #{@config_dir}" }
53
+ Dir[(@config_dir+File::SEPARATOR).squeeze('/')+'*'].sort { |a,b|
54
+ a.split(File::SEPARATOR)[-1][1..2].to_i <=> b.split(File::SEPARATOR)[-1][1..2].to_i
55
+ }.each do |conf|
56
+ next if conf =~ /\/[^\/]*\.[^\/]*$/
57
+ if FileTest.file?(conf)
58
+ @log.debug { "\tadding config file: #{conf}" }
59
+ @config.append( eval( File.open(conf).read ) )
60
+ end
61
+ end
62
+ @pcb = @config[:pcb]
63
+ @config[:mount_point].gsub!(%r{(.)/$},'\1')
64
+ end
65
+
66
+ def initialize( args={ :log_level => Logger::INFO,
67
+ :test_mode => false,
68
+ :config_dir => CONFIG_D,
69
+ :log_file => LOG_FILE,
70
+ :status_file_pref => STATUS_FILE_PREF,
71
+ :source_prefix => '',
72
+ :target_dir_name => nil,
73
+ :delay => nil,
74
+ :delay_denom => nil,
75
+ :no_delete => false,
76
+ :no_acl => false,
77
+ :max_iter => 5,
78
+ } )
79
+ log_level, @test_mode, config_dir, log_file,
80
+ @status_file_pref, @source_prefix, @target_dir_name, @delay, @delay_denom,
81
+ @no_delete, @no_acl, @max_iter =
82
+ args.values_at( :log_level, :test_mode, :config_dir, :log_file,
83
+ :status_file_pref, :source_prefix, :target_dir_name, :delay, :delay_denom,
84
+ :no_delete, :no_acl, :max_iter )
85
+
86
+ @log = nil
87
+ begin
88
+ @log = Logger.new( log_file == '-' ? STDOUT : log_file )
89
+ rescue => e #Errno::EACCES, Errno::EROFS
90
+ $stderr.puts "Exception #{e.class.to_s}: #{e.message}."
91
+ $stderr.puts "\tUsing STDERR for logging."
92
+ @log = Logger.new( STDERR )
93
+ @log.debug("TEST")
94
+ end
95
+ @log.level = log_level
96
+ @log.info { "Running in test mode." } if @test_mode
97
+
98
+ @exclude_list = []
99
+ @last_file = nil
100
+ @config_dir = config_dir
101
+ load_config
102
+ end
103
+
104
+ def config_init( name )
105
+ @backup_name = name
106
+ name = name.intern if name.class==String
107
+
108
+ @status_file = @status_file_pref + '-' + @backup_name
109
+
110
+ # otestujeme, jestli je definovana vybrana zaloha zadaneho jmena
111
+ if not @config.key?(name)
112
+ @log.fatal { "No config for backup '#{@backup_name}', exiting!" }
113
+ return INVALID_BACKUP
114
+ end
115
+ @log.debug{ $HAVE_ACL ? "ACL support" : "no ACL support" }
116
+
117
+ # @cur_config bude obsahovat konfiguraci vybrane zalohy
118
+ @cur_config = @config[name]
119
+ @cur_config[:mount_point].gsub!(%r{(.)/$},'\1') if @cur_config.key?(:mount_point)
120
+
121
+ # find partition by label if necessary
122
+ if conf_val(:device_label) or conf_val(:device_uuid)
123
+ label = conf_val(:device_label) ? conf_val(:device_label) : conf_val(:device_uuid)
124
+ device = find_dev_by( conf_val(:device), label,
125
+ conf_val(:device_label) ? :label : :uuid )
126
+ disk = disk_for_device( device )
127
+ @log.debug { "label/uuid: #{label.inspect}, device: #{device}, disk: #{disk}" }
128
+ @log.error { "no device with given label/uuid #{label.inspect}" } unless device
129
+
130
+ return DEVICE_NOT_FOUND if device.nil? or disk.nil?
131
+ @cur_config[:device], @cur_config[:disk] = [device, disk]
132
+ end
133
+ return 0
134
+ end
135
+
136
+ #### hledani starych backupu ####
137
+ def find_backups( name=nil, any=false ) # TODO --- pripravit na moznost hodinovych zaloh
138
+ dest = nil
139
+ hourly = nil
140
+ # budto mame zadano jmeno zalohy, na kterou se ptame, nebo se pouzije ta,
141
+ # s kterou bylo spusteno run_backup
142
+ if name
143
+ name_s = name.class == String ? name.intern : name
144
+ if !@config.key?(name_s)
145
+ @log.fatal { "No config for backup '#{name_s.to_s}'" }
146
+ return []
147
+ end
148
+ dest = @config[name_s].key?(:target_dir) ? @config[name_s][:target_dir] : conf_val(:mount_point)
149
+ hourly = @config[name_s][:hourly]
150
+ else
151
+ name = @target_dir_name
152
+ dest = @cur_config.key?(:target_dir) ? @cur_config[:target_dir] : conf_val(:mount_point)
153
+ hourly = @cur_config[:hourly]
154
+ end
155
+ if any
156
+ return Dir["#{dest}/backup/*/*/*"].sort
157
+ else
158
+ return Dir["#{dest}/backup/*/*/*/#{name}"].sort
159
+ end
160
+ end
161
+
162
+ #### kopirovaci rutiny ####
163
+ def do_cp_a( src, dest )
164
+ find_exclude?(src)
165
+
166
+ @log.debug { "cp -a #{src} #{dest}" }
167
+
168
+ if !@test_mode
169
+ ret, out, err = system_catch_stdin_stderr('/bin/cp', '-a', src, dest)
170
+ if ret != 0
171
+ @log.info { "/bin/cp failed: '#{err}' (#{src}), making space..." }
172
+
173
+ make_some_space if @space_calc.much_used?
174
+
175
+ ret, out, err = system_catch_stdin_stderr('/bin/cp', '-a', src, dest)
176
+ if ret != 0
177
+ @log.error { "do_cp_a: file copy failed ( '/bin/cp', '-a', #{src}, #{dest} ), out: #{out}, error: #{err}" }
178
+ @log.error { "\t #{src} not copied" }
179
+ end
180
+ end
181
+ end
182
+ end
183
+
184
+ def do_cp_dir(src, dest)
185
+ find_exclude?(src + '/')
186
+
187
+ @log.debug { "FileUtils.mkpath #{dest}" }
188
+
189
+ if !@test_mode
190
+ begin
191
+ FileUtils.mkpath(dest)
192
+ rescue Errno::ENOSPC
193
+ @log.info { "FileUtils.mkpath: Errno::ENOSPC (#{src}), making space..." }
194
+ make_some_space and retry
195
+ end
196
+ end
197
+ end
198
+
199
+ def do_hardlink(last, src, dest)
200
+ find_exclude?(src)
201
+
202
+ @log.debug { "File.link #{last}, #{dest}" }
203
+ if !@test_mode
204
+ begin
205
+ File.link(last, dest)
206
+ rescue Errno::ENOSPC
207
+ @log.info { "File.link: Errno::ENOSPC (#{src}), making space..." }
208
+ make_some_space and retry
209
+ rescue => err
210
+ @log.error { "File.link (#{last} -> #{dest}) error: #{err.message} --> skipping from backup" } # TODO: nastavit indikator chyby!
211
+ end
212
+ end
213
+ end
214
+
215
+ def do_copy(src,dest)
216
+ find_exclude?(src)
217
+
218
+ @log.debug { "do_copy( #{src}, #{dest})" }
219
+
220
+ begin
221
+ # nejdrive udelame misto
222
+ make_space_for(src)
223
+ # pak kopirujeme
224
+ copy_preserve( src, dest ) if !@test_mode
225
+
226
+ rescue Errno::ENOSPC # behem kopirovani nastala vyjimka -- doslo misto
227
+ @log.info { "copy_preserve: Errno::ENOSPC (#{src}), making space..." }
228
+ make_some_space and # nejdriv udelame nejake misto, protoze make_space_for
229
+ # nemusi nic smazat, napriklad pokud je mezitim zdroj
230
+ # soubor smazan
231
+ make_space_for(src) and retry
232
+ # pak udelame misto pro dany soubor (muze byl
233
+ # veliky) a zkusime to znovu
234
+ rescue SysCopyFailure => err
235
+ # syscopy selhal
236
+ @log.info { "copy_preserve: SysCopyFailure (#{src}), checking space..." }
237
+ # muze se stat, ze selze, protoze nema misto, nebo z neznameho duvodu (treba Errno::EIO)
238
+ begin
239
+ if @space_calc.can_backup?(src)
240
+ # mistem to nebylo --> chyba
241
+ @log.error { "copy_preserve: SysCopyFailure (#{src}), enough space --> skipping file" }
242
+ return false
243
+ else
244
+ # TODO: sem by se vubec nemelo dojit (na dojiti mista je samostatna vyjimka ENOSPC)
245
+ # asi nam nekdo pod rukama zabral misto, zkusime znovu
246
+ @log.info { "copy_preserve: Making space (#{src})..." }
247
+ make_space_for(src) and retry
248
+ end
249
+ rescue => e
250
+ @log.error { "do_copy: exception #{e.class}: #{e.message} when handling exception" }
251
+ @log.error { "skipping file #{src}" }
252
+ return false
253
+ end
254
+ rescue Errno::EOVERFLOW
255
+ # soubor je moc velky, neni zazalohovan
256
+ @log.error { "copy_preserve: Errno::EOVERFLOW (#{src}), file too large --> skipping file" }
257
+ return false
258
+ rescue Errno::ENOENT
259
+ # soubor nam zmizel pod rukama behem zalohovani
260
+ @log.warn { "copy_preserve: Errno::ENOENT (#{src}), file deleted during backup" }
261
+ return false
262
+ end
263
+ return true
264
+ end
265
+
266
+ #
267
+ # resolve symlinks in file path
268
+ #
269
+ def realpath(file)
270
+ if not File.respond_to?(:readlink) then
271
+ return file
272
+ end
273
+
274
+ file = File.expand_path(file)
275
+ return file if file == '/'
276
+
277
+ total = ''
278
+ file.split(File::SEPARATOR).each do |comp|
279
+ next if comp.empty?
280
+ total << File::SEPARATOR + comp
281
+ if File.symlink?(total) then
282
+ begin
283
+ total = File.expand_path( File.readlink(total), File.dirname(total) )
284
+ end while File.symlink?(total)
285
+ end
286
+ end
287
+
288
+ return total
289
+ end
290
+
291
+ #### uvolnovani mista ####
292
+ def make_space_for( path )
293
+ while not @space_calc.can_backup?(path) do
294
+ @log.debug { "make_space_for: Not enough space for #{path}, removing oldest." }
295
+ remove_oldest
296
+ end
297
+ end
298
+
299
+ def make_some_space
300
+ remove_oldest
301
+ while @space_calc.much_used?
302
+ remove_oldest
303
+ end
304
+ end
305
+
306
+ def remove_oldest
307
+ if @no_delete
308
+ @log.fatal { "backup device is full, terminating..." }
309
+ raise MakeSpaceFailure.new(DEVICE_FULL)
310
+ end
311
+ backups = find_backups(nil,true)
312
+
313
+ oldest = nil
314
+ # hledame nejstarsi zalohu, ktera neni na seznamu chranenych
315
+ backups.each do |backup|
316
+ if not @dont_delete.index(backup)
317
+ oldest = backup
318
+ break
319
+ end
320
+ @log.debug { "not deleting #{backup}" }
321
+ end
322
+
323
+ if oldest
324
+ oldest.sub!("/#{@backup_name}$",'')
325
+ if FileTest.directory?(oldest)
326
+ @log.info { "free blocks: %s files: %s" % @space_calc.get_free }
327
+ @log.info { "removing oldest: '#{oldest}'" }
328
+
329
+ if not @test_mode
330
+ if Process.euid != 0
331
+ @log.debug { "/bin/chmod -R u+rwX #{oldest}" }
332
+ system('/bin/chmod', '-R', 'u+rwX', oldest)
333
+ end
334
+ @log.debug { "/bin/rm -rf #{oldest}" } or raise MakeSpaceFailure.new(FAILED_TO_FREE_SPACE)
335
+ system('/bin/rm', '-rf', oldest) or raise MakeSpaceFailure.new(FAILED_TO_FREE_SPACE)
336
+
337
+ # mazeme prazdne adresare
338
+ begin
339
+ oldest.sub!(%r'/\d\d$','')
340
+ Dir.rmdir(oldest) # mazeme mesic
341
+ oldest.sub!(%r'/\d\d$','')
342
+ Dir.rmdir(oldest) # mazeme rok
343
+ rescue
344
+ # odchytávame výjimku mazání neprázdného adresáře
345
+ end
346
+ end
347
+ @log.info { "removing oldest done" }
348
+ @log.info { "free blocks: %s files: %s" % @space_calc.get_free }
349
+ return true
350
+ end
351
+ @log.fatal { "remove_oldest: FileTest.directory?(#{oldest}) failed, not removing (processing file: #{@last_file})" }
352
+ else
353
+ @log.fatal { "remove_oldest: No backup to be removed found (processing file: #{@last_file})." }
354
+ end
355
+
356
+ @log.fatal { "remove_oldest failed" }
357
+ raise MakeSpaceFailure.new(FAILED_TO_FREE_SPACE)
358
+ end
359
+
360
+ #### spusteni programu se zachycenim stdout a stderr ####
361
+ def _system_catch_stdin_stderr(*args)
362
+ args.unshift( nil )
363
+ system_catch_stdin_stderr_with_input( args )
364
+ end
365
+
366
+ def system_catch_stdin_stderr(*args)
367
+ args = args.collect {|a| a.to_s}
368
+
369
+ pipe_peer_in, pipe_me_out = IO.pipe
370
+ pipe_me_in, pipe_peer_out = IO.pipe
371
+ pipe_me_error_in, pipe_peer_error_out = IO.pipe
372
+
373
+ pid = nil
374
+ begin
375
+ Thread.exclusive do
376
+ STDOUT.flush
377
+ STDERR.flush
378
+
379
+ pid = fork {
380
+ STDIN.reopen(pipe_peer_in)
381
+ STDOUT.reopen(pipe_peer_out)
382
+ STDERR.reopen(pipe_peer_error_out)
383
+ pipe_me_out.close
384
+ pipe_me_in.close
385
+ pipe_me_error_in.close
386
+
387
+ begin
388
+ exec(*args)
389
+ rescue
390
+ exit!(255)
391
+ end
392
+ }
393
+ end
394
+ end
395
+
396
+ pipe_peer_in.close
397
+ pipe_peer_out.close
398
+ pipe_peer_error_out.close
399
+ pipe_me_out.sync = true
400
+
401
+ pipe_me_out.close
402
+ got_stdin = pipe_me_in.read
403
+ pipe_me_in.close unless pipe_me_in.closed?
404
+ got_stderr = pipe_me_error_in.read
405
+ pipe_me_error_in.close unless pipe_me_error_in.closed?
406
+
407
+ p, status = Process.waitpid2(pid)
408
+ return [status >> 8, got_stdin, got_stderr]
409
+ end
410
+
411
+ def system_catch_stdin_stderr_with_input(input, *args)
412
+ args = args.collect {|a| a.to_s}
413
+
414
+ pipe_peer_in, pipe_me_out = IO.pipe
415
+ pipe_me_in, pipe_peer_out = IO.pipe
416
+ pipe_me_error_in, pipe_peer_error_out = IO.pipe
417
+
418
+ pid = nil
419
+ begin
420
+ Thread.exclusive do
421
+ STDOUT.flush
422
+ STDERR.flush
423
+
424
+ pid = fork {
425
+ STDIN.reopen(pipe_peer_in)
426
+ STDOUT.reopen(pipe_peer_out)
427
+ STDERR.reopen(pipe_peer_error_out)
428
+ pipe_me_out.close
429
+ pipe_me_in.close
430
+ pipe_me_error_in.close
431
+
432
+ begin
433
+ exec(*args)
434
+ rescue
435
+ exit!(255)
436
+ end
437
+ }
438
+ end
439
+ end
440
+
441
+ pipe_peer_in.close
442
+ pipe_peer_out.close
443
+ pipe_peer_error_out.close
444
+
445
+ pipe_me_out.sync = true
446
+ pipe_me_out.print( input ) if input != nil
447
+ pipe_me_out.close
448
+
449
+ got_stdin = pipe_me_in.read
450
+ pipe_me_in.close unless pipe_me_in.closed?
451
+ got_stderr = pipe_me_error_in.read
452
+ pipe_me_error_in.close unless pipe_me_error_in.closed?
453
+
454
+ p, status = Process.waitpid2(pid)
455
+ return [status >> 8, got_stdin, got_stderr]
456
+ end
457
+
458
+ # volani tune2fs
459
+ def tune2fs(dev)
460
+ @log.debug { "calling tune2fs on #{dev}" }
461
+ if Process.euid != 0
462
+ ret, out, err = system_catch_stdin_stderr( '/usr/bin/sudo', '/sbin/tune2fs', '-l', dev )
463
+ else
464
+ ret, out, err = system_catch_stdin_stderr( '/sbin/tune2fs', '-l', dev )
465
+ end
466
+ if ret != 0
467
+ @log.error { "tune2fs failed with exit code: '#{ret}'" }
468
+ @log.error { "\tstdout: #{out}" }
469
+ @log.error { "\tstderr: #{err}" }
470
+ return {}
471
+ end
472
+ results = {}
473
+ out.split("\n").each do |line|
474
+ if line =~ /^([^:]+):\s*(.*)$/
475
+ results[$1] = $2
476
+ end
477
+ end
478
+ return results
479
+ end
480
+
481
+ # resetovani pocitadla mountu
482
+ def reset_mount_count( dev ) # TODO error handling
483
+ @log.info { "reseting mount count on #{dev}" }
484
+ ret, out, err = system_catch_stdin_stderr( '/sbin/tune2fs', '-C', '0', dev )
485
+ if ret != 0
486
+ @log.error { "tune2fs failed with exit code: '#{ret}'" }
487
+ @log.error { "\tstdout: #{out}" }
488
+ @log.error { "\tstderr: #{err}" }
489
+ return false
490
+ end
491
+ return true
492
+ end
493
+
494
+ def disk_for_device( device )
495
+ # TODO: udelat poradne
496
+ if device =~ %r{^(/dev/[hs]d[a-z])[0-9]$}
497
+ return $1
498
+ elsif device =~ %r{^(/dev/.*?)p[0-9]$}
499
+ return $1
500
+ end
501
+ return nil
502
+ end
503
+
504
+ # nastavovani labelu: tune2fs -L MUFF2 /dev/sdb1
505
+ def find_dev_by( device_mask, label, by=:label )
506
+ found = false
507
+ device = nil
508
+ if Array === device_mask
509
+ devices = device_mask
510
+ else
511
+ devices = Dir[ device_mask ]
512
+ end
513
+
514
+ File.readlines('/proc/partitions')[2..-1].each do |line|
515
+ major, minor, blocks, name = line.sub(/^\s+/,'').split(/\s+/)
516
+
517
+ dev = File.join('/dev', name)
518
+ # test na masku
519
+ next unless devices.index( dev )
520
+
521
+ if FileTest.blockdev?( dev )
522
+ @log.debug( "find_dev_by: checking #{dev}" )
523
+ dump = tune2fs( dev )
524
+ if (
525
+ ((by == :label) && (String === label) && ( dump['Filesystem volume name'] =~ /^#{label}$/ )) ||
526
+ ((by == :label) && (Array === label) && ( label.index(dump['Filesystem volume name'] ))) ||
527
+ ((by == :uuid) && (String === label) && ( dump['Filesystem UUID'] == label )) ||
528
+ ((by == :uuid) && (Array === label) && ( label.index(dump['Filesystem UUID'] )))
529
+ )
530
+ if found
531
+ @log.error{ "found at least two devices with given label #{label.inspect}: #{dev} and #{device}" }
532
+ return nil
533
+ else
534
+ found = true
535
+ device = dev
536
+ end
537
+ end
538
+ end
539
+ end
540
+ return device
541
+ end
542
+
543
+ #### pomocne rutiny pro praci s crypto loop ####
544
+ def find_loop(max=8)
545
+ ret, out, err = system_catch_stdin_stderr('/sbin/losetup', '-a')
546
+ return nil if ret != 0
547
+
548
+ loops = out.collect do |l|
549
+ (dev,x) = l.split(':',2)
550
+ dev.sub(%r|^/dev/loop|,'').to_i
551
+ end
552
+
553
+ for i in 0..max
554
+ if not loops.index(i)
555
+ return "/dev/loop#{i}"
556
+ end
557
+ end
558
+ return nil
559
+ end
560
+
561
+ def mk_loop( dev, passwd, size=0, crypto='aes-256' )
562
+ if dev !~ %r|/dev/|
563
+ system_catch_stdin_stderr( '/bin/dd', 'if=/dev/zero', "of=#{dev}", 'bs=1M', "count=#{size}" )
564
+ end
565
+
566
+ loop_dev = find_loop
567
+ system_catch_stdin_stderr_with_input( passwd+"\n", '/sbin/losetup', '-p', '0', '-e', crypto, loop_dev, dev )
568
+ system_catch_stdin_stderr( 'mkfs.ext3', loop_dev )
569
+ system_catch_stdin_stderr( '/sbin/losetup', '-d', loop_dev )
570
+ end
571
+
572
+ #### pomocne File rutiny ####
573
+ def restore_dir_attributes(dirs,depth=0)
574
+ #@log.debug { "restore_dir_attributes: depth=#{depth}" }
575
+ return dirs.find_all do |dir, stat, access_acl, default_acl, d|
576
+ next true if d<depth
577
+
578
+ @log.debug { "chown, utime, chmod #{dir}" }
579
+ if not @test_mode
580
+ begin
581
+ File.chown(stat.uid, stat.gid, dir)
582
+ rescue => error
583
+ @log.warn { "restore_attributes: chown #{dir} got error and didn't restore owner': #{error.message}\n" }
584
+ end
585
+ begin
586
+ File.chmod(stat.mode, dir)
587
+ rescue => error
588
+ @log.warn { "restore_attributes: chmod #{dir} got error and didn't restore rights': #{error.message}\n" }
589
+ end
590
+ if $HAVE_ACL and not @no_acl
591
+ begin
592
+ access_acl.set_file(dir) if access_acl
593
+ default_acl.set_default(dir) if default_acl
594
+ rescue => error
595
+ @log.warn { "restore_attributes: couldn't restore ACLs: #{error.message}\n" }
596
+ end
597
+ end
598
+ begin
599
+ File.utime(stat.atime, stat.mtime, dir)
600
+ rescue => error
601
+ @log.warn { "restore_attributes: utime #{dir} got error and didn't restore times': #{error.message}\n" }
602
+ end
603
+ end
604
+ false
605
+ end
606
+ end
607
+
608
+ def copy_preserve( from, to )
609
+ begin
610
+ if not File.syscopy2( from, to ) # Errno::ENOSPC se siri ven
611
+ File.unlink( to )
612
+ raise SysCopyFailure.new( "syscopy2 returned false when copying #{from} --> #{to}" )
613
+ end
614
+ rescue Errno::EIO => e # tak tohle znamena poradnej pruser....
615
+ msg = "i/o error copying #{from} : #{e.message}"
616
+ @log.error { msg }
617
+ raise SysCopyFailure( msg )
618
+ rescue Errno::ETXTBSY => e # (windows) maji zamklej soubor?
619
+ msg = "text file busy #{from} : #{e.message}"
620
+ @log.error { msg }
621
+ raise SysCopyFailure.new( msg )
622
+ end
623
+
624
+ st = nil
625
+ begin
626
+ st = File.stat( from )
627
+ rescue => error
628
+ @log.warn { "copy_preserve: File.stat #{from} got error and failed: #{error.message}\n" }
629
+ @log.warn { "copy_preserve: NOT restoring owner, rights, times and ACLs on #{to}\n" }
630
+ end
631
+
632
+ if st
633
+ begin
634
+ File.chown( st.uid, st.gid, to )
635
+ rescue => error
636
+ @log.warn { "copy_preserve: chown #{to} got error and didn't restore owner: #{error.message}\n" }
637
+ end
638
+ begin
639
+ File.chmod( st.mode, to )
640
+ rescue => error
641
+ @log.warn { "copy_preserve: chmod #{to} got error and didn't restore rights: #{error.message}\n" }
642
+ end
643
+ if $HAVE_ACL and not @no_acl
644
+ begin
645
+ acl = get_access_acl( from )
646
+ acl.set_file( to ) if acl
647
+ rescue => error
648
+ @log.warn { "copy_preserve: setfacl #{to} got error and didn't restore ACLs: #{error.message}\n" }
649
+ end
650
+ end
651
+ begin
652
+ File.utime( st.atime, st.mtime, to )
653
+ rescue => error
654
+ @log.warn { "copy_preserve: utime #{to} got error and didn't restore times: #{error.message}\n" }
655
+ end
656
+ end
657
+ end
658
+
659
+ def same_file?(f1, f2)
660
+ begin
661
+ result = ( !File.symlink?(f1) and
662
+ !File.symlink?(f2) and
663
+ File.file?(f1) and
664
+ File.file?(f2) and
665
+ (s1 = File.stat(f1)).size == (s2 = File.stat(f2)).size and
666
+ ( (s1.mtime - s2.mtime) <= 1 ) and
667
+ s1.uid == s2.uid and
668
+ s1.gid == s2.gid and
669
+ s1.mode == s2.mode )
670
+
671
+ return result unless result
672
+
673
+ if $HAVE_ACL and not @no_acl
674
+ return false unless ACL.from_file(f1).to_text == ACL.from_file(f2).to_text
675
+ # default ACL neresime, protoze tady mame jen soubory
676
+ #return false unless ACL.default(f1).to_text == ACL.default(f2).to_text
677
+ end
678
+ return true
679
+ rescue => error
680
+ @log.warn { "same_file?(#{f1}, #{f2}) got error and returned false: #{error.message}\n" }
681
+ return false
682
+ end
683
+ end
684
+
685
+ #### rizeni chodu Find ####
686
+
687
+ # hledame 1. match a podle neho se ridime, cili pod klicem :exclude ZALEZI NA PORADI
688
+ # a na predni mista davame konkretnejsi pravidla a az na ne pravidla obecnejsi
689
+ def find_exclude?( path )
690
+ return if @skip_excludes
691
+ # spocteme si relativni cestu
692
+ rel_path = path.dup
693
+ if rel_path.index( @exclude_root ) == 0
694
+ if @exclude_root == '/'
695
+ rel_path[0,1] = ''
696
+ else
697
+ rel_path[0,@exclude_root.length+1] = '' # odmazeme vcetne lomitka
698
+ end
699
+ else
700
+ rel_path = nil # muzeme nastavit nil, protoze nil =~ /cokoliv/ je false
701
+ end
702
+ # matchujeme
703
+ @exclude_list.each do |a|
704
+ if (a[2] ? path : rel_path) =~ a[1]
705
+ if a[0]
706
+ @log.debug { "excluding(#{a[1].source}): #{path}" }
707
+ Find.prune
708
+ else
709
+ return
710
+ end
711
+ end
712
+ end
713
+ end
714
+
715
+ # absolute=>true bude se matchovat absolutni (cela cesta)
716
+ # absolute=>false bude se matchovat cesta relativni ke klici :dir
717
+ def set_exclude(str, absolute=false)
718
+ if str == '!'
719
+ @exclude_list = []
720
+ return
721
+ end
722
+ exclude_pattern = true
723
+ if str =~ /^\+\s*(.*)$/
724
+ exclude_pattern = false
725
+ str = $1
726
+ elsif str =~ /^-\s*(.*)$/
727
+ exclude_pattern = true
728
+ str = $1
729
+ end
730
+ @exclude_list << [exclude_pattern, Regexp.new('^'+str), absolute]
731
+ end
732
+
733
+ def umount_fsck_mount
734
+ if umount_backup
735
+ check_fsck or return print_error_stats( FSCK_FAILED )
736
+ end
737
+ mount_backup or return print_error_stats( MOUNT_FAILED )
738
+ return 0
739
+ end
740
+
741
+ #### zalohovani se vsim vsudy ####
742
+ def go_backup( name, no_mirror )
743
+ if ((res = config_init(name)) != 0) || ((res = umount_fsck_mount) != 0)
744
+ return res
745
+ end
746
+
747
+ res = run_backup
748
+ if (res == BACKUP_OK)
749
+ # pokud probehla v poradku 1. faze muzeme pokracovat mirrorem
750
+ if not no_mirror
751
+ mirror_res = create_mirror
752
+ res = mirror_res unless mirror_res == nil
753
+ end
754
+ else
755
+ print_error_stats( res )
756
+ end
757
+
758
+ umount_backup(true)
759
+ return res
760
+ end
761
+
762
+ def detect_running(lock_file_name)
763
+ if FileTest.exists?(lock_file_name)
764
+ @log.debug { "detect_running: lock file #{lock_file_name} exists" }
765
+ pid = File.open(lock_file_name).read(nil).chomp.to_i rescue nil
766
+ if pid == nil
767
+ @log.error { "detect_running: invalid PID in #{lock_file_name}" }
768
+ @log.warn { "detect_running: assuming lnbackup not running" }
769
+ return false
770
+ end
771
+ if FileTest.exists?(cmd_file="/proc/#{pid}/cmdline")
772
+ cmd_line = File.open(cmd_file).read(nil) rescue ''
773
+ @log.debug { "detect_running: process ##{pid}' command line: #{cmd_line}" }
774
+ if (cmd_line =~ /\/lnbackup/)
775
+ @log.error { "lnbackup already running, pid: #{pid}, command line: #{cmd_line}" }
776
+ return true
777
+ else
778
+ @log.warn { "lnbackup probably not running lock_file_name: #{lock_file_name}" }
779
+ @log.warn { "\t\tpid: #{pid}, command line: #{cmd_line}" }
780
+ return false
781
+ end
782
+ else
783
+ @log.debug { "detect_running: #{lock_file_name} does not exist" }
784
+ return false
785
+ end
786
+ else
787
+ return false
788
+ end
789
+ end
790
+
791
+ # Hledame v predchozich zalohach soubor jmena podle tmp_src.
792
+ # Divame, jestli nalezeny souboj je "stejny" jako ten, ktery
793
+ # mame zalohovat.
794
+ # Pokud ano, vratime ho, jinak vratime nil.
795
+ # Hledani zarazime, pokud najdeme soubor na stejne ceste,
796
+ # ktery neni "stejny"
797
+ #
798
+ # TODO: hledani by se melo take zastavit, kdyz se podivame do posledni
799
+ # dokoncene zalohy -- na to potrebujeme strojive zpracovatelny status
800
+ # file pro predchozi zalohy --> ukol k reseni
801
+ def find_same_prev_backup( src, tmp_src, all_backups )
802
+ max_iter = @max_iter
803
+ all_backups.reverse.each do |prev_backup|
804
+ last = File.expand_path(tmp_src, prev_backup)
805
+ return last if same_file?(src,last)
806
+ return false if FileTest.exists?(last)
807
+
808
+ max_iter -= 1
809
+ return false if max_iter == 0
810
+ end
811
+ return nil
812
+ end
813
+
814
+ def get_access_acl(file) ($HAVE_ACL and not @no_acl) ? (ACL::from_file(file) rescue nil) : nil; end
815
+ def get_default_acl(file) ($HAVE_ACL and not @no_acl) ? (ACL::default(file) rescue nil) : nil; end
816
+
817
+ #### hlavni zalohovaci rutina ####
818
+ def run_backup
819
+ begin
820
+ @log.info { "running backup #{@backup_name}" }
821
+
822
+ @skip_excludes = false
823
+ lock_file = nil
824
+ pre_command_ok = true
825
+
826
+ @backup_start = Time.now
827
+
828
+ @stats = {
829
+ :size => 0, :total_size => 0, :blocks => 0,
830
+ :f_same => 0, :f_changed => 0, :f_new => 0,
831
+ :file => 0, :dir => 0, :symlink => 0, :blockdev => 0, :chardev => 0, :socket => 0, :pipe => 0,
832
+ }
833
+
834
+ # POZOR: behovy zamek delame az PO vykonani pre-command
835
+
836
+ # podivame se po pripadnem prikazu, ktery bychom meli udelat pred zalohovanim
837
+ if @cur_config.key?(:pre_command)
838
+ ret, out, err = system_catch_stdin_stderr(@cur_config[:pre_command])
839
+ if ret != 0
840
+ @log.fatal { "pre command failed: '#{@cur_config[:pre_command]}" }
841
+ @log.fatal { "\texit code: '#{ret}'" }
842
+ @log.fatal { "\tstdout: #{out}" }
843
+ @log.fatal { "\tstderr: #{err}" }
844
+ pre_command_ok = false
845
+ return PRE_COMMAND_FAILED
846
+ end
847
+ end
848
+
849
+ # cilovy adresar pro zalohy je budto gobalni mount_point nebo target_dir
850
+ # vybrane zalohy + datum + (hodina) + nazev zalohy
851
+ pre_dest_tmp = @cur_config.key?(:target_dir) ? @cur_config[:target_dir] : conf_val(:mount_point)
852
+ if not File.directory?(pre_dest_tmp)
853
+ @log.fatal { "backup root '#{pre_dest_tmp}' does not exist" }
854
+ return NO_BACKUP_DIR
855
+ end
856
+ begin
857
+ lock_file_name = File.join(pre_dest_tmp,'LNBACKUP_RUNNING')
858
+ return LNBACKUP_RUNNING if detect_running(lock_file_name)
859
+ lock_file = File.open( lock_file_name, 'w' )
860
+ lock_file.puts($$)
861
+ lock_file.flush
862
+ rescue => e
863
+ return PIDFILE_FAILED
864
+ end
865
+
866
+ # udaj o rezervovanem miste bereme prednosti z konkretni zalohy,
867
+ # pokud neni nakonfigurovan, tak globalne
868
+ files_reserved = conf_val(:files_reserved)
869
+ blocks_reserved = conf_val(:blocks_reserved)
870
+ @space_calc = FreeSpaceCalc.new( pre_dest_tmp, files_reserved, blocks_reserved, @log )
871
+
872
+ dest_tmp = File.join( pre_dest_tmp, 'backup').squeeze('/')
873
+ @log.debug { "dest_tmp: #{dest_tmp}" }
874
+
875
+ # nemazeme posledni backup
876
+ @dont_delete = []
877
+ prev_backup = ( all_backups = find_backups )[-1]
878
+ if prev_backup
879
+ @dont_delete << prev_backup.gsub(/\/[^\/]*$/,'')
880
+ end
881
+
882
+ @dest = File.join( dest_tmp,
883
+ Date.today.backup_dir(@cur_config[:hourly]),
884
+ @target_dir_name ).squeeze('/')
885
+ # a nemazeme ani to, co zrovna zalohujeme
886
+ @dont_delete << @dest.gsub(/\/[^\/]*$/,'')
887
+
888
+ if File.directory?(@dest)
889
+ @log.fatal { "today's/this hour's backup (#{@dest}) already exists --> exiting" }
890
+ return BACKUP_EXISTS
891
+ else
892
+ begin
893
+ begin
894
+ FileUtils.mkpath(@dest) unless @test_mode
895
+ rescue Errno::ENOSPC
896
+ make_some_space and retry
897
+ end
898
+ rescue => e
899
+ @log.fatal { "can't make backup dir '#{@dest}'" }
900
+ @log.fatal { "mkpath #{@dest} raised exception #{e.class}:'#{e.message}'" }
901
+ @log.fatal { e.backtrace.join("\n") }
902
+ return NO_BACKUP_DIR
903
+ end
904
+ end
905
+
906
+ @log.debug { "previous backup: #{prev_backup}" }
907
+ @log.debug { "dont_delete: #{@dont_delete.join(',')}" }
908
+
909
+ # adresare je nutno vyrobit, nez do nich nasypeme soubory,
910
+ # ale nastavit jejich atributy musime az po souborech,
911
+ dirs_to_restore = []
912
+
913
+ dirs = Array.new
914
+ dirs_hash = Hash.new
915
+ @cur_config[:dirs].each do |dir|
916
+ tmp_dir = @source_prefix ? File.join( @source_prefix, dir[:dir] ) : dir[:dir]
917
+ dirs << [ rp = realpath(tmp_dir).gsub(/(.)\/+$/,'\1') , dir[:fs_type], dir[:exclude] ]
918
+ dirs_hash[rp] = true
919
+ end
920
+
921
+ dirs.each do |path,fs_type,exclude|
922
+ path = File.expand_path(path)
923
+
924
+ # inicializace excludes
925
+ @exclude_root = path
926
+ @exclude_list = []
927
+ exclude.each { |ex| set_exclude(ex) } if exclude
928
+ @exclude_list.unshift( [true, /^#{Regexp.escape(dest_tmp)}\/./, true] )
929
+ @exclude_list.unshift( [true, /^#{Regexp.escape(lock_file_name)}$/, true] )
930
+ @log.debug { "Excluding: #{@exclude_list.to_s}" }
931
+ @log.debug { "exclude_root: #{@exclude_root}" }
932
+
933
+ # overime, ze zdroj existuje
934
+ begin
935
+ File.stat(path)
936
+ rescue Errno::ENOENT
937
+ @log.error { "path #{path} not found --> skipping from backup" }
938
+ next
939
+ rescue Errno::EACCES
940
+ @log.error { "path #{path} no rights to access file --> skipping from backup" }
941
+ next
942
+ rescue => error
943
+ @log.error { "path #{path} : File.stat got error #{error.class} : #{error.message} --> skipping from backup" }
944
+ next
945
+ end
946
+
947
+ # projit cestu, udelat linky a adresare
948
+ @log.debug { "resolving links for #{path}" }
949
+ if path != '/'
950
+ file = path
951
+
952
+ total = ''
953
+ file.split(File::SEPARATOR).each do |piece|
954
+ next if piece == ""
955
+ total << File::SEPARATOR + piece
956
+ if File.symlink?(total) then
957
+ begin
958
+ # dokud se jedna o symlink, tak musime linkovat
959
+ do_cp_a(total, File.join(@dest,total))
960
+
961
+ total = File.expand_path( File.readlink(total), File.dirname(total) )
962
+ end while File.symlink?(total)
963
+ # a nakonec udelame adresar
964
+ else
965
+ # pokud se nejedna o symlink, udelame adresar
966
+ tgt = File.join(@dest,total).squeeze('/')
967
+ do_cp_dir( total, tgt )
968
+ # ulozime si ho do seznamu pro pozdejsi nastaveni vlastnosti
969
+ dirs_to_restore << [ tgt, File.stat(total), get_access_acl(total), get_default_acl(total), 0 ]
970
+ end
971
+ end
972
+ path = total
973
+ end
974
+
975
+ # overime, ze existuje i cesta, kam nas zavedly odkazy a
976
+ # ulozime si device pro budouci porovnavani
977
+ dev = nil
978
+ begin
979
+ dev = File.stat(path).dev
980
+ rescue Errno::ENOENT
981
+ @log.error { "path #{path} not found --> skipping from backup" }
982
+ next
983
+ end
984
+
985
+ # overime si, ze mame tento adresar zalohovat (napr. home pres nfs)
986
+ @log.debug { "checking fs_type for #{path}" }
987
+ if (fs_type == :local) and (dev <= 700) # HRUBY ODHAD --> TODO: poradne
988
+ @log.info { "#{path} is not local (dev=#{dev}), skipping" }
989
+ next
990
+ end
991
+
992
+ @log.debug { "running on directory #{path}" }
993
+ last_depth = 0
994
+ Find.find3( path ) do |src,depth|
995
+ if @delay_denom
996
+ if @delay and (rand(@delay_denom) == 0)
997
+ sleep(@delay)
998
+ end
999
+ elsif @delay
1000
+ sleep(@delay)
1001
+ end
1002
+ dirs_to_restore = restore_dir_attributes(dirs_to_restore, depth+1) if last_depth>depth
1003
+ last_depth = depth
1004
+
1005
+ @last_file = src
1006
+ src = File.expand_path('./'+src, '/')
1007
+ tmp_src = './' + src
1008
+ dest = File.expand_path(tmp_src, @dest)
1009
+
1010
+ begin
1011
+ file_stat = File.lstat(src)
1012
+ rescue => error
1013
+ @log.error { "can not stat #{src}: #{error.class}: #{error.message} --> skipping from backup" }
1014
+ Find.prune
1015
+ end
1016
+
1017
+ # POZOR !!
1018
+ # nelze volat file_stat.readable?, protoze:
1019
+ # irb(main):016:0* File.lstat('/etc/asterisk/cdr_manager.conf').readable?
1020
+ # => false
1021
+ # irb(main):017:0>
1022
+ # irb(main):018:0*
1023
+ # irb(main):019:0* File.readable?('/etc/asterisk/cdr_manager.conf')
1024
+ # => true
1025
+ # irb(main):020:0> system('ls -l /etc/asterisk/cdr_manager.conf')
1026
+ # -rw-rw---- 1 asterisk asterisk 59 2005-12-08 00:58 /etc/asterisk/cdr_manager.conf
1027
+ # => true
1028
+
1029
+ # if not file_stat.readable? and not file_stat.symlink?
1030
+ if not File.readable?(src) and not file_stat.symlink?
1031
+ @log.error { "can not read #{src} --> skipping from backup" }
1032
+ Find.prune
1033
+ end
1034
+
1035
+ if file_stat.symlink? or file_stat.blockdev? or file_stat.chardev? or
1036
+ file_stat.socket? or file_stat.pipe?
1037
+ # symlink, blockdev, chardev, socket, pipe zalohujeme pomoci 'cp -a'
1038
+ do_cp_a(src, dest)
1039
+ if file_stat.symlink?
1040
+ @stats[:symlink] += 1
1041
+ elsif file_stat.blockdev?
1042
+ @stats[:blockdev] += 1
1043
+ elsif file_stat.chardev?
1044
+ @stats[:chardev] += 1
1045
+ elsif file_stat.socket?
1046
+ @stats[:socket] += 1
1047
+ elsif file_stat.pipe?
1048
+ @stats[:pipe] += 1
1049
+ end
1050
+ elsif file_stat.directory?
1051
+ # adresar
1052
+
1053
+ # preskocime koren, protoze uz je vytvoren
1054
+ if src != path
1055
+ # preskocime v pripade vicenasobneho dosazeni stejneho adresare
1056
+ Find.prune if dirs_hash.key?(src)
1057
+ @stats[:dir] += 1
1058
+
1059
+ do_cp_dir(src, dest)
1060
+ # ulozime si ho do seznamu pro pozdejsi nastaveni vlastnosti
1061
+ dirs_to_restore << [dest, file_stat, get_access_acl(src), get_default_acl(src), depth]
1062
+ end
1063
+
1064
+ # kontrola zastaveni noreni pri zmene device
1065
+ if dev != (new_dev = file_stat.dev)
1066
+ @log.debug { "filesystem border: #{src} old=#{dev}, new=#{new_dev}" }
1067
+ case fs_type
1068
+ when :local
1069
+ # pri zmene device otestujeme, ze je lokalni
1070
+ # konec pokud device neni lokalni
1071
+ if new_dev <= 700 # HRUBY ODHAD --> TODO: poradne
1072
+ @log.info { "#{src} is not local (dev=#{new_dev}), skipping" }
1073
+ Find.prune
1074
+ end
1075
+ # 0x300 ---> ide
1076
+ # 2304 ---> md0
1077
+ # 26625 ---> hwraid
1078
+ # 9 --> autofs
1079
+ # 7 --> pts
1080
+ # 2 --> proc
1081
+ # 0xa --> nfs
1082
+ when :single
1083
+ # single --> koncime, jakmile je podadresar z jineho device
1084
+ Find.prune
1085
+ when :all
1086
+ # all --> bereme vsechno
1087
+ end
1088
+ end
1089
+ else # normalni soubor
1090
+ @stats[:file] += 1
1091
+ @stats[:total_size] += file_stat.size
1092
+ # overime, jestli mame drivejsi zalohu souboru a jestli nebyl zmenen
1093
+ backuped = false
1094
+ new = false
1095
+ last = nil
1096
+ if prev_backup
1097
+ # hledat file i v predchozich zalohach: viz. TODO OTESTOVAT!!
1098
+ #last = File.expand_path(tmp_src, prev_backup)
1099
+ #if same_file?(src,last)
1100
+
1101
+ # hledame v drivejsich zalohach ...
1102
+ last = find_same_prev_backup(src, tmp_src, all_backups)
1103
+ if last # last neni ani 'nil', ano 'false'
1104
+ do_hardlink(last, src, dest)
1105
+ backuped = true
1106
+ @stats[:f_same] += 1
1107
+ end
1108
+ end
1109
+ if not backuped
1110
+ # pokud jsme prosli az sem, budeme soubor kopirovat
1111
+ if do_copy(src,dest)
1112
+ if prev_backup
1113
+ if last.nil? # nil --> vubec jsme nenasli
1114
+ @stats[:f_new] += 1
1115
+ @log.debug { "new file: #{src}" }
1116
+ else # false --> nasli jsme, ale byl zmenen
1117
+ @stats[:f_changed] += 1
1118
+ @log.debug { "changed file: #{src}" }
1119
+ end
1120
+ else
1121
+ # nehlasime 'new file' u 1. zaloh
1122
+ @stats[:f_new] += 1
1123
+ end
1124
+
1125
+ if @test_mode
1126
+ # v test modu odhadujeme velikost podle zdroje
1127
+ @stats[:size] += file_stat.size
1128
+ @stats[:blocks] += file_stat.blocks
1129
+ else
1130
+ @stats[:size] += File.size(dest)
1131
+ @stats[:blocks] += File.stat(dest).blocks
1132
+ end
1133
+ end
1134
+ end
1135
+ end
1136
+ end
1137
+ end
1138
+ restore_dir_attributes(dirs_to_restore)
1139
+
1140
+ over_all_status = BACKUP_OK
1141
+
1142
+ @log.info { "Backup '#{@target_dir_name}' complete succesfully." }
1143
+ print_stats( over_all_status )
1144
+ rescue MakeSpaceFailure => e
1145
+ return e.code # in this case we return the error code given by exception
1146
+ # ( possibly ignoring status code from post_command )
1147
+ ensure
1148
+ if lock_file
1149
+ begin
1150
+ lock_file.close
1151
+ ensure
1152
+ File.delete( lock_file_name )
1153
+ end
1154
+ end
1155
+
1156
+ if pre_command_ok and @cur_config.key?(:post_command)
1157
+ ret, out, err = system_catch_stdin_stderr(@cur_config[:post_command])
1158
+ if ret != 0
1159
+ @log.error { "post command failed: '#{@cur_config[:post_command]}" }
1160
+ @log.error { "\texit code: '#{ret}'" }
1161
+ @log.error { "\tstdout: #{out}" }
1162
+ @log.error { "\tstderr: #{err}" }
1163
+ over_all_status = POST_COMMAND_FAILED
1164
+ end
1165
+ end
1166
+ end
1167
+ return over_all_status
1168
+ end
1169
+
1170
+ def print_stats( code )
1171
+ sum = 0
1172
+ [:file, :dir, :symlink, :blockdev, :chardev, :socket, :pipe].each { |s| sum += @stats[s] }
1173
+
1174
+ status = []
1175
+ status << "lnbackup statistics for: #{@target_dir_name}"
1176
+ status << "backup config: #{@backup_name}"
1177
+ status << "overall status code: #{code}"
1178
+ status << "overall status message: #{MESSAGES[code]}"
1179
+ status << "running in: TEST MODE" if @test_mode
1180
+ status << ""
1181
+ status << "backup start: #{@backup_start}"
1182
+ status << "backup end: #{Time.now}"
1183
+ status << "total size: #{@stats[:total_size]}"
1184
+ status << "backup size: #{@stats[:size]}"
1185
+ status << "backup blocks: #{@stats[:blocks]}"
1186
+ status << ""
1187
+ status << "files unmodified: #{@stats[:f_same]}"
1188
+ status << "files changed: #{@stats[:f_changed]}"
1189
+ status << "files new: #{@stats[:f_new]}"
1190
+ status << ""
1191
+ status << "total objects: #{sum}"
1192
+ status << "files: #{@stats[:file]}"
1193
+ status << "dirs: #{@stats[:dir]}"
1194
+ status << "symlinks: #{@stats[:symlink]}"
1195
+ status << "blockdevs: #{@stats[:blockdev]}"
1196
+ status << "chardevs: #{@stats[:chardev]}"
1197
+ status << "sockets: #{@stats[:socket]}"
1198
+ status << "pipes: #{@stats[:pipe]}"
1199
+ print_status_array(status)
1200
+ end
1201
+
1202
+ def print_error_stats(err)
1203
+ status = []
1204
+ status << "error code: #{err}"
1205
+ status << "message: #{MESSAGES[err]}"
1206
+ print_status_array(status)
1207
+ return err
1208
+ end
1209
+
1210
+ # prints status information to log file, stdout and to status file
1211
+ def print_status_array( status, mode='w' )
1212
+ @log.info { "Status information follows" }
1213
+ status_hash = {}
1214
+ status.each do |line|
1215
+ puts line
1216
+ @log << line + "\n"
1217
+
1218
+ key, value = line.chomp.split(':',2).collect{ |a| a.strip }
1219
+ status_hash[key] = value
1220
+ end
1221
+ status_hash['uuid'] = @part_uuid
1222
+ status_hash['label'] = @part_label
1223
+ begin
1224
+ File.open(@status_file,mode) do |f|
1225
+ status.each { |line| f.puts line }
1226
+ end
1227
+ rescue => error
1228
+ @log.error { "error writing status file #{@status}: #{error.message}" }
1229
+ end
1230
+ # binary status file
1231
+ begin
1232
+ # read the status array
1233
+ status_array = Marshal.restore( File.open(@status_file+'.bin').read(nil) ) rescue []
1234
+
1235
+ # adding to last status information -- take the last status info, modify it and write back
1236
+ if mode == 'a'
1237
+ status_array[0] ||= {}
1238
+ status_array[0].update( status_hash )
1239
+
1240
+ # writing new status information -- add new element to the beginning
1241
+ else
1242
+ status_array.unshift( status_hash )
1243
+ end
1244
+
1245
+ # write the status array
1246
+ File.open(@status_file+'.bin','w') { |f| f << Marshal.dump( status_array ) }
1247
+ rescue => error
1248
+ @log.error { "error writing binary status file #{@status}: #{error.message}" }
1249
+ end
1250
+ end
1251
+
1252
+ def parse_stats
1253
+ stats = {}
1254
+ File.open(@status_file,'r').readlines.each do |line|
1255
+ key, value = line.chomp.split(':',2).collect{ |a| a.strip }
1256
+ stats[key] = value
1257
+ end
1258
+ return stats
1259
+ end
1260
+
1261
+ def size2str (size)
1262
+ size = size.to_i
1263
+ if size < 1024
1264
+ "#{size} "
1265
+ elsif size < 1048576
1266
+ "%.1fK" % (size / 1024.0)
1267
+ elsif size < 1073741824
1268
+ "%.1fM" % (size / 1048576.0)
1269
+ else
1270
+ "%.1fG" % (size / 1073741824.0)
1271
+ end
1272
+ end
1273
+
1274
+ def backup_partitions
1275
+ devices = Dir[ @config[:device] ]
1276
+ backups = @config.keys.find_all {|k| Hash===@config[k] and @config[k].key?(:dirs) and not @config[k][:dont_check] }
1277
+ out_backup_devices = []
1278
+ devs = {}
1279
+ for backup in backups
1280
+ devs[backup] = devices
1281
+ if @config[backup][:device]
1282
+ devs[backup] = Dir[ @config[backup][:device] ]
1283
+ end
1284
+ if @config[backup][:device_label]
1285
+ devs[backup] = [ find_dev_by(devs[backup], @config[backup][:device_label], :label) ]
1286
+ end
1287
+ if @config[backup][:device_uuid]
1288
+ devs[backup] = [ find_dev_by(devs[backup], @config[backup][:device_uuid], :uuid) ]
1289
+ end
1290
+ out_backup_devices |= devs[backup]
1291
+ end
1292
+ return out_backup_devices
1293
+ end
1294
+
1295
+ def backup_partition?(partition)
1296
+ return backup_partitions.include?(partition)
1297
+ end
1298
+
1299
+ def nagios_check_all
1300
+ #backups = @config[:check_backups] || [:localhost]
1301
+ total_message = ''
1302
+ total_message_bad = ''
1303
+ worst_result = 0
1304
+ for backup in @config.keys.find_all {|k| Hash===@config[k] and @config[k].key?(:dirs) and not @config[k][:dont_check] }
1305
+ result, message = nagios_check( backup.to_s )
1306
+ total_message << '[' << message << '] '
1307
+ total_message_bad << '[' << message << '] ' if result != 0
1308
+ worst_result = result > worst_result ? result : worst_result
1309
+ end
1310
+ total_message = total_message_bad if worst_result != 0
1311
+ return [worst_result, total_message]
1312
+ end
1313
+
1314
+ def nagios_check( backup_name = 'localhost' )
1315
+ @status_file = @status_file_pref + '-' + backup_name
1316
+ conf_key = backup_name.intern
1317
+
1318
+ if not @config.key?(conf_key)
1319
+ return [ 2, "backup '#{backup_name}' not configured" ]
1320
+ end
1321
+
1322
+ no_mirror_warn = false
1323
+ warn_t = 26 # implicitni hodnoty pro warning a error (v hodinach)
1324
+ error_t = 30
1325
+ if @config[conf_key].key?(:monitor)
1326
+ warn_t = @config[conf_key][:monitor][:warn] || warn_t
1327
+ error_t = @config[conf_key][:monitor][:error] || error_t
1328
+ no_mirror_warn = @config[conf_key][:monitor][:no_mirror_warn] || false
1329
+ end
1330
+
1331
+ stats = {}
1332
+ message = ''
1333
+ status = 0
1334
+
1335
+ if File.readable?(@status_file)
1336
+ stats = parse_stats
1337
+ else
1338
+ return [ 2, "backup status file #{@status_file} not found" ]
1339
+ end
1340
+
1341
+ if stats.key?('backup end')
1342
+ message = stats['lnbackup statistics for'] + ': '
1343
+
1344
+ delta = ((Time.now() - Time.parse( stats['backup end'] ))/3600).to_i
1345
+ if delta > error_t
1346
+ status = 2
1347
+ message << "backup age: #{delta}h > #{error_t} ->ERR"
1348
+ elsif delta > warn_t
1349
+ status = 1
1350
+ message << "backup age: #{delta}h > #{warn_t} ->WARN"
1351
+ else
1352
+ if stats.key?('mirror end')
1353
+ message << (Time.parse( stats['mirror end'] ).strftime( '%H:%M:%S %d/%m/%y' ) rescue 'parse error')
1354
+ message << ' : ' << size2str(stats['backup size'].to_i) << '/' << size2str(stats['total size']) << 'B '
1355
+ message << '(bootable)'
1356
+ else
1357
+ message << stats['backup end']
1358
+ message << ' : ' << size2str(stats['backup size'].to_i) << '/' << size2str(stats['total size']) << 'B '
1359
+ if @config[backup_name.intern][:mirror]
1360
+ if no_mirror_warn
1361
+ message << '(not bootable ->warning canceled by config)'
1362
+ else
1363
+ message << '(not bootable ->WARN)'
1364
+ status = 1 if status < 1
1365
+ end
1366
+ end
1367
+ end
1368
+ end
1369
+
1370
+ else
1371
+ message = backup_name.to_s + ' : '
1372
+ if stats.key?("error code")
1373
+ status = (s = stats['error code'].to_i) > status ? s : status
1374
+ message << stats['message']
1375
+ else
1376
+ message << "Unknown error"
1377
+ stats = 2
1378
+ end
1379
+ end
1380
+
1381
+ return [ status<2 ? status : 2, message ]
1382
+ rescue
1383
+ raise
1384
+ return [ 2, "can't parse status file #{@status_file}" ]
1385
+ end
1386
+
1387
+ def do_mirror_only(name)
1388
+ if (res = config_init(name)) != 0
1389
+ return res
1390
+ end
1391
+
1392
+ @status_file = '/dev/null'
1393
+ @dest = find_backups[-1]
1394
+ create_mirror
1395
+ end
1396
+
1397
+ #### vytvoreni mirroru
1398
+ # pousti se po run_backup, takze muze pocitat s pripravenym prostredim...
1399
+ def create_mirror
1400
+ # pokud nema vybrana konfigurace v konfiguraci povoleno mirrorovani,
1401
+ # vratime nil
1402
+ return nil unless @cur_config[:mirror] == true
1403
+
1404
+ # pokud nemame device, nebo mount_point, nebo jsou prazdne, tak to rozhodne zabalime
1405
+ return nil if conf_val(:device).to_s.empty? or conf_val(:mount_point).to_s.empty?
1406
+
1407
+ # taky to zabalime, jestlize mame zadano crypto (neumime bootovat z sifrovane zalohy)
1408
+ return nil if @config.key?(:crypto)
1409
+
1410
+ mnt = conf_val(:mount_point)
1411
+ part = conf_val(:device)
1412
+ disk = conf_val(:disk)
1413
+
1414
+ lock_file_name = File.join(mnt,'LNBACKUP_RUNNING')
1415
+ return LNBACKUP_RUNNING if detect_running(lock_file_name)
1416
+ lock_file = File.open( lock_file_name, 'w' )
1417
+ lock_file.puts($$)
1418
+ lock_file.flush
1419
+
1420
+ @log.info { "creating bootable mirror #{@backup_name} at #{part} mounted as #{mnt}" }
1421
+
1422
+ # smazat stary mirror
1423
+ command = [ 'find', "#{mnt}", '-xdev',
1424
+ '-maxdepth', '1', '-mindepth', '1',
1425
+ '!', '-name', 'backup',
1426
+ '!', '-name', 'LNBACKUP_RUNNING',
1427
+ '!', '-name', 'lost+found',
1428
+ '-exec', 'rm', '-fr', '{}', ';' ]
1429
+ @log.debug { 'running: ' + command.join(' ') }
1430
+ system( *command ) unless @test_mode
1431
+
1432
+ @exclude_list = []
1433
+ @skip_excludes = true
1434
+
1435
+ @dont_delete = [@dest]
1436
+ latest_mirror = @dest
1437
+ latest_mirror_len = latest_mirror.size
1438
+ target_dir = conf_val(:mount_point)
1439
+
1440
+ # vytvorit novy mirror
1441
+ dirs_to_restore = []
1442
+ last_depth = 0
1443
+ Find.find3(latest_mirror + '/') do |src,depth|
1444
+ if @delay_denom
1445
+ if @delay and (rand(@delay_denom) == 0)
1446
+ sleep(@delay)
1447
+ end
1448
+ elsif @delay
1449
+ sleep(@delay)
1450
+ end
1451
+ dirs_to_restore = restore_dir_attributes(dirs_to_restore, depth+1) if last_depth>depth
1452
+ last_depth = depth
1453
+
1454
+ src = File.expand_path('./' + src, '/')
1455
+ tmp_dst = './' + src[latest_mirror_len..-1]
1456
+ dest = File.expand_path(tmp_dst, target_dir)
1457
+
1458
+ next if src==latest_mirror
1459
+
1460
+ begin
1461
+ if File.symlink?(src) or File.blockdev?(src) or File.chardev?(src) or File.socket?(src) or File.pipe?(src)
1462
+ # symlink, blockdev, chardev, socket, pipe zalohujeme pomoci 'cp -a'
1463
+ do_cp_a(src, dest)
1464
+ elsif File.directory?(src)
1465
+ # adresar
1466
+ # preskocime koren, protoze uz je vytvoren
1467
+ next if src == latest_mirror
1468
+ do_cp_dir(src, dest)
1469
+ # ulozime si ho do seznamu pro pozdejsi nastaveni vlastnosti
1470
+ dirs_to_restore << [ dest, File.stat(src), get_access_acl(src), get_default_acl(src), depth ] ### TODO: CHYBA?
1471
+ else # normalni soubor --> hard link
1472
+ do_hardlink(src, src, dest)
1473
+ end
1474
+ end
1475
+ end
1476
+ restore_dir_attributes(dirs_to_restore)
1477
+
1478
+ # vypnout lnbackup na zazalohovane kopii
1479
+ # mv "$MNT"/etc/cron.daily/lnbackup{,.disabled}
1480
+ # ucinit system bootovatelnym, pokud mame zadan i disk
1481
+ ret = true
1482
+ if disk
1483
+ if not @test_mode
1484
+ ret = create_fstab( mnt, part, disk )
1485
+
1486
+ if ret and not @cur_config[:skip_lilo]
1487
+ if FileTest.executable?(LILO_PATH)
1488
+ ret = create_lilo_conf( mnt, part, disk )
1489
+ else
1490
+ @log.warn { "LILO binary '#{LILO_PATH}' not found, skipping LILO" }
1491
+ end
1492
+ else
1493
+ @log.info { "skipping LILO as requested in config" }
1494
+ end
1495
+ end
1496
+ else
1497
+ @log.error { "'disk' not defined, skipping LILO and fstab ..." }
1498
+ ret = false
1499
+ end
1500
+
1501
+ status = [""]
1502
+ if ret
1503
+ @log.info { "Mirror '#{@backup_name}' finished." }
1504
+ status << "mirror end: #{Time.now}"
1505
+ else
1506
+ @log.error { "Mirror '#{@backup_name}' failed." }
1507
+ status << "mirror failed: #{Time.now}"
1508
+ end
1509
+
1510
+ print_status_array(status,'a')
1511
+ return BACKUP_OK
1512
+ ensure
1513
+ if lock_file
1514
+ begin
1515
+ lock_file.close
1516
+ ensure
1517
+ File.delete( lock_file_name )
1518
+ end
1519
+ end
1520
+ end
1521
+
1522
+ #### generovani souboru
1523
+ # TODO: pokud je zaloha udelana pres label, generovat fstab na label
1524
+ # i swap lze nalezt pres label v /dev/disk/by-uuid/
1525
+ #
1526
+ # priklad /etc/fstab
1527
+ # UUID=ad71a12d-9cdf-460e-beab-969533237a36 none swap defaults 0 0
1528
+ # UUID=b76083c3-2871-4719-a670-26848043686d / ext3 defaults 0 0
1529
+
1530
+ def create_fstab( mnt, part, disk )
1531
+ swap_line = `/sbin/fdisk -l #{disk} | grep 'Linux swap'`.chomp
1532
+ swap_n = ( ( swap_line =~ /^#{disk}(\d+).*$/ ) ? $1 : nil ).to_s
1533
+ part_n = ( ( part =~ /^#{disk}(\d+)$/ ) ? $1 : nil ).to_s
1534
+ fstab = "#{mnt}/etc/fstab"
1535
+
1536
+ # check fstab
1537
+ if part_n == ''
1538
+ @log.error { "Cannot find correct part_n='#{part_n}'!" }
1539
+ @log.error { "The file '#{fstab}' was not generated." }
1540
+ @log.error { "Backup not bootable!" }
1541
+ return false
1542
+ end
1543
+ if swap_n == ''
1544
+ @log.warn { "Cannot find correct swap_n='#{swap_n}" }
1545
+ @log.warn { "The file '#{fstab}' was generated WITHOUT SWAP partition." }
1546
+ @log.warn { "Backuped system may have problems !" }
1547
+ end
1548
+
1549
+ ext3_options = 'noatime,defaults,errors=remount-ro'
1550
+ ext3_options << ',acl,user_xattr' if $HAVE_ACL and not @no_acl
1551
+
1552
+ # create fstab
1553
+ File.unlink(fstab)
1554
+ File.open(fstab,'w') do |f| # TODO: generate fstab based of filesystem LABELs (if possible, swap?)
1555
+ # otherwise we don't boot system from SATA/SCSI disks
1556
+ f.puts '#<file system> <mount point> <type> <options> <dump> <pass>'
1557
+ f.puts "/dev/hda#{part_n} / ext3 #{ext3_options} 0 1"
1558
+ f.puts "/dev/hda#{swap_n} none swap sw 0 0" if swap_n != ''
1559
+ f.puts "proc /proc proc defaults 0 0"
1560
+ end
1561
+
1562
+ return true
1563
+ end
1564
+
1565
+ def create_lilo_conf( mnt, part, disk )
1566
+ # run lilo
1567
+ sys_lilo_cfg = "/etc/lilo.conf" # TODO relativni k necemu?
1568
+ lilo_cfg = File.join( mnt, sys_lilo_cfg )
1569
+
1570
+
1571
+ File.unlink( lilo_cfg )
1572
+ File.open( lilo_cfg, 'w' ) do |f|
1573
+ f.puts "disk=#{disk}\nbios=0x80"
1574
+ content = File.open( sys_lilo_cfg ).read(nil)
1575
+ f.print content.gsub(/^((raid-extra-boot|disk|bios)[[:space:]]*=)/, '#\1')
1576
+ end
1577
+
1578
+ lilo_ret, lilo_out, lilo_err = system_catch_stdin_stderr( LILO_PATH, '-r', mnt, '-b', disk )
1579
+ if lilo_ret != 0 # check lilo
1580
+ @log.error { "error (#{lilo_ret}) in: '/sbin/lilo -r #{mnt} -b #{disk}' --> system not bootable" }
1581
+ @log.error { "\tstdout: #{lilo_out}" }
1582
+ @log.error { "\tstderr: #{lilo_err}" }
1583
+ return false
1584
+ else
1585
+ return true
1586
+ end
1587
+ end
1588
+
1589
+ #### kontrola disku a spousteni fsck ####
1590
+ def check_fsck
1591
+ part = nil
1592
+ crypto = @config[:crypto]
1593
+ password = @config[:password]
1594
+ loop_dev = nil
1595
+
1596
+ begin
1597
+ part = conf_val(:device)
1598
+ if (crypto)
1599
+ loop_dev = find_loop
1600
+ system_catch_stdin_stderr_with_input( password+"\n", '/sbin/losetup',
1601
+ '-p', '0', '-e', crypto, loop_dev, part )
1602
+ part = loop_dev
1603
+ end
1604
+
1605
+ if ! part.to_s.empty?
1606
+ @log.debug { "running: /sbin/tune2fs -l #{part}" }
1607
+ fs_params = {}
1608
+
1609
+ ret, out, err = system_catch_stdin_stderr('/sbin/tune2fs', '-l', part)
1610
+ if ret == 0
1611
+ out.split("\n").each do |l|
1612
+ k,v = l.split(/\s*:\s*/)
1613
+ fs_params[k] = v
1614
+ end
1615
+ else
1616
+ @log.error { "tune2fs failed with exit code: '#{ret}'" }
1617
+ @log.error { "\tstdout: #{out}" }
1618
+ @log.error { "\tstderr: #{err}" }
1619
+ return false
1620
+ end
1621
+
1622
+ if (mount_count = fs_params['Mount count'].to_i)+5 >=
1623
+ (max_mount_count = fs_params['Maximum mount count'].to_i)
1624
+ @log.info { "Disk #{part} has reached mount_count: #{mount_count} (max: #{max_mount_count}), running fsck" }
1625
+ if not @test_mode
1626
+ ret, out, err = system_catch_stdin_stderr( '/sbin/fsck.ext3', '-y', part )
1627
+ if ret == 0
1628
+ @log.info { "fsck found no errors" }
1629
+ return reset_mount_count( part )
1630
+ elsif ret == 1
1631
+ @log.info { "fsck corrected some errors" }
1632
+ @log.debug { "\tstdout: #{out}" }
1633
+ @log.debug { "\tstderr: #{err}" }
1634
+ return reset_mount_count( part )
1635
+ else
1636
+ @log.error { "fsck failed with exit code: '#{ret}'" }
1637
+ @log.error { "\tstdout: #{out}" }
1638
+ @log.error { "\tstderr: #{err}" }
1639
+ return false
1640
+ end
1641
+ end
1642
+ else
1643
+ @log.debug { "Disk #{part}: mount_count: #{mount_count} (max: #{max_mount_count}), not running fsck" }
1644
+ end
1645
+ @part_uuid = fs_params['Filesystem UUID']
1646
+ @part_label = fs_params['Filesystem volume name']
1647
+ @log.info { "Disk: #{part}, UUID: #{@part_uuid}, Label: #{@part_label}" }
1648
+ end
1649
+ ensure
1650
+ if crypto and loop_dev
1651
+ system_catch_stdin_stderr( '/sbin/losetup', '-d', loop_dev )
1652
+ end
1653
+ end
1654
+
1655
+ return true
1656
+ end
1657
+
1658
+ #### vypsani stavu zalohy ####
1659
+ def backup_status( backup_name, status_file_pref = nil )
1660
+ @status_file_pref = status_file_pref if status_file_pref
1661
+ res = nil
1662
+ return res if (res = config_init(backup_name)) != 0
1663
+
1664
+ if File.readable?(@status_file)
1665
+ puts File.open(@status_file).read(nil)
1666
+ puts
1667
+ end
1668
+ mount_backup
1669
+ backups = find_backups( backup_name )
1670
+ backups.each do |backup|
1671
+ puts backup
1672
+ end
1673
+ umount_backup(true)
1674
+ end
1675
+
1676
+ #### mount/umount ####
1677
+ # :none - not mounted, :ro - mounted ro, :rw - mounted rw
1678
+ def check_mounted
1679
+ File.open('/proc/mounts').read(nil).each_line do |line|
1680
+ what, where, type, opts, dump, pass = line.split(/\s+/)
1681
+ return [ opts.split(',').index('rw') ? :rw : :ro, what ] if where == conf_val(:mount_point)
1682
+ end
1683
+ return [ :none, nil ]
1684
+ end
1685
+
1686
+ def mount_backup(rw = true)
1687
+ device = conf_val(:device)
1688
+ mount_point = conf_val(:mount_point)
1689
+ fstype = conf_val(:fs_type)
1690
+ crypto = @config[:crypto]
1691
+ password = @config[:password]
1692
+ remount = false
1693
+
1694
+ if ! device.to_s.empty? and ! mount_point.to_s.empty?
1695
+
1696
+ mount_status, mount_dev = check_mounted
1697
+ @log.debug { "mount status before mount: device: #{mount_dev}, status: #{mount_status}" }
1698
+
1699
+ # pokud je na nasem mountpointu namountovano cokoliv jineho, nez co ma byt, koncime
1700
+ if (mount_status != :none) and (mount_dev != device)
1701
+ @log.fatal { "wrong device mounted as #{mount_point}: have #{mount_dev}, need #{device}"}
1702
+ return false
1703
+ end
1704
+
1705
+ if mount_status != :none
1706
+ if (mount_status == :ro) and rw
1707
+ # mame namontovano, ale jen ro, musime remountovat
1708
+ remount = true
1709
+ else
1710
+ # mame namontovano rw, ale neudelali jsme si to sami, preskocime mounting
1711
+ @log.warn { "disk not remounted, using previously mounted disk!" }
1712
+ return true
1713
+ end
1714
+ end
1715
+
1716
+ # try mount
1717
+ @log.debug { "mounting #{device} --> #{mount_point} " +
1718
+ (rw ? (remount ? 'remount,RW' : 'RW') : 'RO') +
1719
+ (crypto ? '[crypto]':'') }
1720
+
1721
+ cmd = [ 'mount', device, mount_point ]
1722
+ if rw
1723
+ if remount
1724
+ cmd << '-o' << 'remount,rw'
1725
+ end
1726
+ cmd << '-o' << 'noatime'
1727
+ else
1728
+ cmd << '-o' << 'ro'
1729
+ end
1730
+ cmd << '-o' << 'acl,user_xattr' if $HAVE_ACL and not @no_acl
1731
+
1732
+ cmd << '-t' << fstype unless fstype.to_s.empty?
1733
+
1734
+ ret, out, err =
1735
+ crypto ? system_catch_stdin_stderr_with_input(
1736
+ *( [password+"\n"] + cmd + ["-oencryption=#{crypto}", '-p', '0'] ) ) :
1737
+ system_catch_stdin_stderr( *cmd )
1738
+ if ret != 0
1739
+ @log.fatal { "mount #{device} --> #{mount_point} failed" }
1740
+ @log.warn { "\tstdout: #{out}" }
1741
+ @log.warn { "\tstderr: #{err}" }
1742
+ return false
1743
+ end
1744
+ end
1745
+ return true
1746
+ end
1747
+
1748
+ # unmount backup
1749
+ # return true on success (not mounted or succesfull unmount)
1750
+ def umount_backup(try_ro=false)
1751
+ device = conf_val(:device)
1752
+ mount_point = conf_val(:mount_point)
1753
+
1754
+ if ! device.to_s.empty? and ! mount_point.to_s.empty?
1755
+
1756
+ mount_status, mount_dev = check_mounted
1757
+ if mount_status != :none
1758
+ @log.debug { "mount status before umount: device: #{mount_dev}, status: #{mount_status}" }
1759
+ @log.debug { "unmounting #{mount_point}" }
1760
+
1761
+ ret, out, err = system_catch_stdin_stderr( '/bin/umount', mount_point )
1762
+ if ret != 0
1763
+ @log.warn { "umount #{mount_point} failed" }
1764
+ @log.warn { "\tstdout: #{out}" }
1765
+ @log.warn { "\tstderr: #{err}" }
1766
+
1767
+ if not try_ro
1768
+ return false
1769
+ else
1770
+ # jeste se pokusime o remount,ro
1771
+ ret, out, err = system_catch_stdin_stderr( '/bin/mount', '-o', 'remount,rw', mount_point )
1772
+ if ret != 0
1773
+ @log.warn { "mount -o remount,ro #{mount_point} failed" }
1774
+ @log.warn { "\tstdout: #{out}" }
1775
+ @log.warn { "\tstderr: #{err}" }
1776
+ @log.warn { "backup remained mounted!" }
1777
+ end
1778
+ end
1779
+ end
1780
+ end
1781
+ end
1782
+ return true
1783
+ end
1784
+
1785
+ def mirror_only( backup_name )
1786
+ return MOUNT_FAILED unless mount_backup
1787
+
1788
+ mirror_res = do_mirror_only( backup_name )
1789
+ res = mirror_res unless mirror_res == nil
1790
+
1791
+ umount_backup
1792
+ return 0
1793
+ end
1794
+
1795
+ def backup_pcs( pcs_status_f, no_delete )
1796
+ @log.info { "starting PCs backup" }
1797
+
1798
+ if (ret = umount_fsck_mount) != 0
1799
+ return ret
1800
+ end
1801
+
1802
+ pc_status = Marshal.restore( File.open( pcs_status_f ).read( nil ) ) rescue {}
1803
+
1804
+ @pcb[:backup_hosts].each do |host|
1805
+ @log.info { "mounting #{host}" }
1806
+ mount_to = "#{@pcb[:mounts_root]}/#{host}"
1807
+ system_catch_stdin_stderr('umount', mount_to )
1808
+ FileUtils.mkpath( mount_to )
1809
+
1810
+ pc_status[host] = Hash.new unless pc_status.key?(host)
1811
+ pc_status[host][:tried] = Time.now
1812
+ pc_status[host][:status] = -1
1813
+
1814
+ cmd = [ "/bin/mount", "-t", "smbfs", "-o",
1815
+ "username=#{@pcb[:backup_user]},"+
1816
+ "password=#{@pcb[:backup_password]},"+
1817
+ "workgroup=#{@pcb[:backup_workgroup]}",
1818
+ "//#{host}/#{@pcb[:backup_share]}", mount_to ]
1819
+ @log.debug { "running '" + cmd.join("' '") + "'" }
1820
+ ret, out, err = system_catch_stdin_stderr( *cmd )
1821
+
1822
+ if ret == 0
1823
+ begin
1824
+ @log.debug { "host #{host} mount passed" }
1825
+
1826
+ # run backup
1827
+ bk2 = LnBackup.new(
1828
+ :log_level => @log.level,
1829
+ :log_file => "#{@pcb[:log_dir]}/lnbackup-#{host}.log",
1830
+ :test_mode => @test_mode,
1831
+ :config_dir => @config_dir,
1832
+ :status_file_pref => "#{@pcb[:status_dir]}/lnbackup-#{host}.status",
1833
+ :source_prefix => mount_to,
1834
+ :target_dir_name => "#{@pcb[:backup_config]}/#{host}",
1835
+ :no_delete => no_delete
1836
+ )
1837
+
1838
+ bk2.config_init( @pcb[:backup_config] ) # TODO: error handling ?!
1839
+ res = bk2.run_backup
1840
+
1841
+ pc_status[host][:status] = res
1842
+ pc_status[host][:stats] = bk2.stats
1843
+ if res == BACKUP_OK
1844
+ pc_status[host][:success] = Time.now
1845
+ end
1846
+ rescue => e
1847
+ @log.fatal { "host #{host} raised exception #{e.class}:'#{e.message}'" }
1848
+ @log.fatal { e.backtrace.join("\n") }
1849
+ @log.fatal { "skipping to next host" }
1850
+ ensure
1851
+ system_catch_stdin_stderr("/bin/umount", mount_to )
1852
+ end
1853
+ @log.debug { "host #{host} finished" }
1854
+ else
1855
+ @log.error { "#{host} failed: \n\tout:#{out}\n\terr:#{err}" }
1856
+ pc_status[host][:status] = SMBMOUNT_FAILED
1857
+ end
1858
+ end
1859
+
1860
+ begin
1861
+ File.open( pcs_status_f, 'w' ) do |f|
1862
+ f.print Marshal.dump( pc_status )
1863
+ end
1864
+ rescue => e
1865
+ @log.error { "cannot write pc_status: #{pcs_status_f}: #{e.message}" }
1866
+ end
1867
+
1868
+ umount_backup(true)
1869
+ @log.info { "finished PCs backup" }
1870
+ end
1871
+
1872
+ def backup_pcs_status( pcs_status_f )
1873
+ pc_status = Hash.new
1874
+ begin
1875
+ pc_status = Marshal.restore( File.open( pcs_status_f ).read( nil ) )
1876
+ rescue
1877
+ end
1878
+
1879
+ print "Content-type: text/html\r\n\r\n"
1880
+ puts "<html><table>"
1881
+
1882
+ @pcb[:backup_hosts].each do |host|
1883
+ puts "<tr><td>#{host}<td>"
1884
+
1885
+ if not pc_status.key?(host)
1886
+ puts "\t\thas no backup"
1887
+ else
1888
+ st = pc_status[host]
1889
+ if st.key?(:status) and (st[:status] == BACKUP_OK)
1890
+ puts "\t\t(OK) " + st[:success].to_s + ' ' + size2str( st[:stats][:size].to_i ) << 'B '
1891
+ else
1892
+ if st.key?(:success)
1893
+ puts "\t\t(WARN) Last success: " + st[:success].to_s
1894
+ else
1895
+ puts "\t\t(ERROR) Last tried: " + st[:tried].to_s
1896
+ end
1897
+ end
1898
+ end
1899
+ end
1900
+
1901
+ puts "</table></html>"
1902
+ end
1903
+
1904
+ def backup_pcs_test
1905
+ @log.info { "starting PCs backup test" }
1906
+
1907
+ @pcb[:backup_hosts].each do |host|
1908
+ @log.debug { "trying #{host} ... " }
1909
+ #echo smbclient -c "''" -W $BACKUP_WORKGROUP //$HOST/$BACKUP_SHARE -U $BACKUP_USER%$BACKUP_PASSWORD
1910
+ cmd = [ '/usr/bin/smbclient', '-c', '', '-W', @pcb[:backup_workgroup],
1911
+ "//#{host}/#{@pcb[:backup_share]}", '-U',
1912
+ "#{@pcb[:backup_user]}%#{@pcb[:backup_password]}" ]
1913
+ @log.debug { "running '" + cmd.join("' '") + "'" }
1914
+
1915
+ ret, out, err = system_catch_stdin_stderr( *cmd )
1916
+ if ret == 0
1917
+ @log.info { "#{host} passed" }
1918
+ else
1919
+ @log.error { "#{host} failed: ret:#{ret}\n\tout:#{out}\n\terr:#{err}" }
1920
+ end
1921
+ end
1922
+
1923
+ @log.info { "finished PCs backup test" }
1924
+ end
1925
+
1926
+ def crypto_init(size)
1927
+ device = conf_val(:device)
1928
+ crypto = @config[:crypto]
1929
+ password = @config[:password]
1930
+
1931
+ if crypto and password and device
1932
+ mk_loop( device, password, size, crypto )
1933
+ else
1934
+ @log.fatal { "crypto_init: must specify device, crypto and password in config file" }
1935
+ end
1936
+ end
1937
+ end
1938
+ end