lnbackup 2.1

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