zfs_mgmt 0.3.10 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a54ed2e0ad255f69367aaeb25fd0dddcc8346cec0fd8be02561e4ea1210510f2
4
- data.tar.gz: 1d31c311f63835c3f7a2b6e771b06628f1901b62130e9ec5d0255ff987fb89bc
3
+ metadata.gz: 558d14c8bb3e9617b2d355308c85cd1da639035fb67662409a6445e1769c6e55
4
+ data.tar.gz: ef0acb4c148281bb81e0df51f08efa9c2ab87302a14c3700cce3214e9ab9e515
5
5
  SHA512:
6
- metadata.gz: c0534143034e6edb6ce1161bacd86765cf9a151812d1849f3570c983691dfdf8d03f5d0aeefb070fa144fa0fa6838711b855e07180a7c70cd14f3f00d561d562
7
- data.tar.gz: dbf53bcfe6223106f080393621d58ef538a2cf9fe9ae9e7f338b2cbe4e5d88d5bca2fcf44f3c6e8ffb76050ce2d0cfc75fa12638b8d19aae759b9b507fab3a65
6
+ metadata.gz: 84f9fa0ba043d4c7a8e4b4625642bd5c14260591438da6851ecf6704a7747baedd4f1e397c3d55a648e55e6588c5bd6efd31e56a243a918bb9cb40de5db26c14
7
+ data.tar.gz: 1d77d1167f829dfa24236c3bdbda28093b15c5f325a55c02c26f9cd8eba7e9d73d1e2db3d8e68c6d9370ecc5e70ec676f2dba9682ab4a05d80bce273572f6206
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- zfs_mgmt (0.3.10)
4
+ zfs_mgmt (0.4.4)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/bin/zfsmgr CHANGED
@@ -8,11 +8,19 @@ class ZfsMgr < Thor
8
8
  class_option :zfs_binary, :type => :string, :default => 'zfs',
9
9
  :desc => 'zfs binary'
10
10
  class_option :zpool_binary, :type => :string, :default => 'zpool',
11
- :desc => 'zfs binary'
11
+ :desc => 'zpool binary'
12
+ class_option :mbuffer_binary, :type => :string, :default => 'mbuffer',
13
+ :desc => 'mbuffer binary'
14
+ class_option :pv_binary, :type => :string, :default => 'pv',
15
+ :desc => 'pv binary'
16
+ class_option :loglevel, :type => :string, :default => ( $stdout.isatty ? 'info' : 'warn' ),
17
+ :enum => ['debug','error','fatal','info','warn'],
18
+ :desc => 'set logging level to specified severity'
12
19
  desc "zfsget [ZFS]", "execute zfs get for the given properties and types and parse the output into a nested hash"
13
20
  method_option :properties, :type => :array, :default => ['name'], :desc => "List of properties passed to zfs get"
14
21
  method_option :types, :type => :array, :default => ['filesystem','volume'], enum: ['filesystem','volume','snapshot'], :desc => "list of types"
15
22
  def zfsget(zfs)
23
+ ZfsMgmt.set_log_level(options[:loglevel])
16
24
  ZfsMgmt.global_options = options
17
25
  pp ZfsMgmt.zfsget(properties: options[:properties],
18
26
  types: options[:types],
@@ -24,6 +32,8 @@ class ZfsMgr < Thor
24
32
  subcommand "list", ZfsMgmt::ZfsMgr::List
25
33
  desc "restic SUBCOMMAND ...ARGS", "backup zfs to restic"
26
34
  subcommand "restic", ZfsMgmt::ZfsMgr::Restic
35
+ desc "send SUBCOMMAND ...ARGS", "send zfs"
36
+ subcommand "send", ZfsMgmt::ZfsMgr::Send
27
37
  end
28
38
 
29
39
  ZfsMgr.start(ARGV)
data/lib/zfs_mgmt.rb CHANGED
@@ -9,7 +9,7 @@ require 'text-table'
9
9
  require 'open3'
10
10
  require 'filesize'
11
11
 
12
- $logger = Logger.new(STDERR)
12
+ $logger = Logger.new(STDERR, progname: 'zfs_mgmt')
13
13
 
14
14
  $date_patterns = {
15
15
  'hourly' => '%F Hour %H',
@@ -41,6 +41,8 @@ module ZfsMgmt
41
41
  class << self
42
42
  attr_accessor :global_options
43
43
  end
44
+ class ZfsGetError < StandardError
45
+ end
44
46
  def self.custom_properties()
45
47
  return [
46
48
  'policy',
@@ -53,6 +55,9 @@ module ZfsMgmt
53
55
  'snapshot',
54
56
  'snap_prefix',
55
57
  'snap_timestamp',
58
+ 'send',
59
+ 'remote',
60
+ 'destination',
56
61
  ].map do |p|
57
62
  ['zfsmgmt',p].join(':')
58
63
  end
@@ -87,8 +92,7 @@ module ZfsMgmt
87
92
 
88
93
  def self.zfs_hold(hold,snapshot)
89
94
  com = [global_options['zfs_binary'], 'hold', hold, snapshot]
90
- $logger.debug("#{com.join(' ')}")
91
- system(com.join(' '))
95
+ system_com(com)
92
96
  unless $?.success?
93
97
  errstr = "unable to set hold: #{hold} for snapshot: #{snapshot}"
94
98
  $logger.error(errstr)
@@ -98,8 +102,7 @@ module ZfsMgmt
98
102
 
99
103
  def self.zfs_release(hold,snapshot)
100
104
  com = [@global_options['zfs_binary'], 'release', hold, snapshot]
101
- $logger.debug("#{com.join(' ')}")
102
- system(com.join(' '))
105
+ system_com(com)
103
106
  unless $?.success?
104
107
  errstr = "unable to release hold: #{hold} for snapshot: #{snapshot}"
105
108
  $logger.error(errstr)
@@ -107,20 +110,20 @@ module ZfsMgmt
107
110
  end
108
111
  end
109
112
 
110
- def self.zfsget(properties: ['name'],types: ['filesystem','volume'],zfs: '')
113
+ def self.zfsget(properties: ['all'],types: ['filesystem','volume'],zfs: '', command_prefix: [])
111
114
  results={}
112
115
  com = [ZfsMgmt.global_options[:zfs_binary], 'get', '-Hp', properties.join(','), '-t', types.join(','), zfs]
113
- $logger.debug(com.join(' '))
114
- so,se,status = Open3.capture3(com.join(' '))
116
+ $logger.debug((command_prefix+com).join(' '))
117
+ so,se,status = Open3.capture3((command_prefix+com).join(' '))
115
118
  if status.signaled?
116
119
  $logger.error("process was signalled \"#{com.join(' ')}\", termsig #{status.termsig}")
117
- raise 'ZfsGetError'
120
+ raise ZfsGetError, "process was signalled \"#{com.join(' ')}\", termsig #{status.termsig}"
118
121
  end
119
122
  unless status.success?
120
123
  $logger.error("failed to execute \"#{com.join(' ')}\", exit status #{status.exitstatus}")
121
124
  so.split("\n").each { |l| $logger.debug("stdout: #{l}") }
122
125
  se.split("\n").each { |l| $logger.error("stderr: #{l}") }
123
- raise 'ZfsGetError'
126
+ raise ZfsGetError, "failed to execute \"#{com.join(' ')}\", exit status #{status.exitstatus}"
124
127
  end
125
128
  so.split("\n").each do |line|
126
129
  params = line.split("\t")
@@ -235,7 +238,7 @@ module ZfsMgmt
235
238
  }
236
239
  return saved,saved_snaps,deleteme
237
240
  end
238
- def self.zfs_managed_list(filter: '.+', properties: custom_properties(), property_match: { 'zfsmgmt:manage' => 'true' } )
241
+ def self.zfs_managed_list(filter: '.+', properties: ['all'], property_match: { 'zfsmgmt:manage' => method(:prop_on?) } )
239
242
  zfss = [] # array of arrays
240
243
  zfsget(properties: properties).each do |zfs,props|
241
244
  unless /#{filter}/ =~ zfs
@@ -243,7 +246,7 @@ module ZfsMgmt
243
246
  end
244
247
  managed = true
245
248
  property_match.each do |k,v|
246
- unless props.has_key?(k) and props[k] == v
249
+ unless key_comp?(props,k,v)
247
250
  managed = false
248
251
  break
249
252
  end
@@ -258,20 +261,20 @@ module ZfsMgmt
258
261
  end
259
262
  return zfss
260
263
  end
261
- def self.snapshot_policy(verbopt: false, debugopt: false, filter: '.+')
262
- if debugopt
263
- $logger.level = Logger::DEBUG
264
- else
265
- $logger.level = Logger::INFO
266
- end
267
- zfs_managed_list(filter: filter).each do |zdata|
268
- (zfs,props,snaps) = zdata
269
- unless props.has_key?('zfsmgmt:policy') and policy_parser(props['zfsmgmt:policy'])
270
- $logger.error("zfs_mgmt is configured to manage #{zfs}, but there is no valid policy configuration, skipping")
264
+ def self.snapshot_policy(filter: '.+')
265
+ zfs_managed_list(filter: filter).each do |zfs,props,snaps|
266
+ unless props.has_key?('zfsmgmt:policy')
267
+ $logger.error("zfs_mgmt is configured to manage #{zfs}, but there is no policy configuration in zfsmgmt:policy, skipping")
271
268
  next # zfs
272
269
  end
273
- # call the function that decides who to save and who to delete
274
- (saved,saved_snaps,deleteme) = snapshot_destroy_policy(zfs,props,snaps)
270
+
271
+ begin
272
+ # call the function that decides who to save and who to delete
273
+ (saved,saved_snaps,deleteme) = snapshot_destroy_policy(zfs,props,snaps)
274
+ rescue ArgumentError
275
+ $logger.error("zfs_mgmt is configured to manage #{zfs}, but there is no valid policy configuration, skipping")
276
+ next
277
+ end
275
278
 
276
279
  if saved_snaps.length == 0
277
280
  $logger.info("no snapshots marked as saved by policy for #{zfs}")
@@ -287,21 +290,20 @@ module ZfsMgmt
287
290
  print table.to_s
288
291
  end
289
292
  end
290
- def self.snapshot_destroy(noop: false, verbopt: false, debugopt: false, filter: '.+')
291
- if debugopt
292
- $logger.level = Logger::DEBUG
293
- else
294
- $logger.level = Logger::INFO
295
- end
296
- zfs_managed_list(filter: filter).each do |zdata|
297
- (zfs,props,snaps) = zdata
298
- unless props.has_key?('zfsmgmt:policy') and policy_parser(props['zfsmgmt:policy'])
299
- $logger.error("zfs_mgmt is configured to manage #{zfs}, but there is no valid policy configuration, skipping")
293
+ def self.snapshot_destroy(noop: false, verbose: false, filter: '.+')
294
+ zfs_managed_list(filter: filter).each do |zfs,props,snaps|
295
+ unless props.has_key?('zfsmgmt:policy')
296
+ $logger.error("zfs_mgmt is configured to manage #{zfs}, but there is no policy configuration in zfsmgmt:policy, skipping")
300
297
  next # zfs
301
298
  end
302
299
 
303
- # call the function that decides who to save and who to delete
304
- (saved,saved_snaps,deleteme) = snapshot_destroy_policy(zfs,props,snaps)
300
+ begin
301
+ # call the function that decides who to save and who to delete
302
+ (saved,saved_snaps,deleteme) = snapshot_destroy_policy(zfs,props,snaps)
303
+ rescue ArgumentError
304
+ $logger.error("zfs_mgmt is configured to manage #{zfs}, but there is no valid policy configuration, skipping")
305
+ next
306
+ end
305
307
 
306
308
  $logger.info("deleting #{deleteme.length} snapshots for #{zfs}")
307
309
  deleteme.reverse! # oldest first for removal
@@ -309,32 +311,22 @@ module ZfsMgmt
309
311
  $logger.debug("delete: #{snap_name} #{local_epoch_to_datetime(snaps[snap_name]['creation']).strftime('%F %T')}")
310
312
  end
311
313
 
312
- com_base = "zfs destroy -p"
313
- if deleteme.length > 0
314
- com_base = "#{com_base}d"
315
- end
316
- if noop
317
- com_base = "#{com_base}n"
318
- end
319
- if verbopt
320
- com_base = "#{com_base}v"
321
- end
314
+ com_base = ['zfs', 'destroy']
315
+ com_base.push('-d') if deleteme.length > 0 # why?
316
+ com_base.push('-n') if noop
317
+ com_base.push('-v') if verbose
322
318
  while deleteme.length > 0
323
319
  for i in 0..(deleteme.length - 1) do
324
320
  max = deleteme.length - 1 - i
325
321
  $logger.debug("attempting to remove snaps 0 through #{max} out of #{deleteme.length} snapshots")
326
322
  bigarg = "#{zfs}@#{deleteme[0..max].map { |s| s.split('@')[1] }.join(',')}"
327
- com = "#{com_base} #{bigarg}"
323
+ com = com_base + [bigarg]
328
324
  $logger.debug("size of bigarg: #{bigarg.length} size of com: #{com.length}")
329
325
  if bigarg.length >= 131072 or com.length >= (2097152-10000)
330
326
  next
331
327
  end
332
- $logger.info(com)
333
328
  deleteme = deleteme - deleteme[0..max]
334
- system(com)
335
- if $?.exitstatus != 0
336
- $logger.error("zfs exited with non-zero status: #{$?.exitstatus}")
337
- end
329
+ system_com(com) # pass -n, always run the command though
338
330
  break
339
331
  end
340
332
  end
@@ -348,7 +340,7 @@ module ZfsMgmt
348
340
  end
349
341
  p = str.scan(/\d+[#{$time_pattern_map.keys.join('')}]/i)
350
342
  unless p.length > 0
351
- raise "unable to parse the policy configuration #{str}"
343
+ raise ArgumentError.new("unable to parse the policy configuration #{str}")
352
344
  end
353
345
  p.each do |pi|
354
346
  scn = /(\d+)([#{$time_pattern_map.keys.join('')}])/i.match(pi)
@@ -356,33 +348,338 @@ module ZfsMgmt
356
348
  end
357
349
  res
358
350
  end
359
- def self.snapshot_create(noop: false, verbopt: false, debugopt: false, filter: '.+')
360
- if debugopt
361
- $logger.level = Logger::DEBUG
351
+ # snapshot all filesystems configured for snapshotting
352
+ def self.snapshot_create(noop: false, filter: '.+')
353
+ dt = DateTime.now
354
+ zfsget.select { |zfs,props|
355
+ # must match filter
356
+ match_filter?(zfs, filter) and
357
+ # snapshot must be on or true
358
+ (
359
+ key_comp?(props,'zfsmgmt:snapshot') or
360
+ # or snapshot can be recursive and local, but only if the source is local or received
361
+ ( key_comp?(props,'zfsmgmt:snapshot',['recursive','local']) and key_comp?(props,'zfsmgmt:snapshot@source',['local','received']) )
362
+ )
363
+ }.each do |zfs,props|
364
+ prefix = ( props.has_key?('zfsmgmt:snap_prefix') ? props['zfsmgmt:snap_prefix'] : 'zfsmgmt' )
365
+ ts = ( props.has_key?('zfsmgmt:snap_timestamp') ? props['zfsmgmt:snap_timestamp'] : '%FT%T%z' )
366
+ com = [global_options['zfs_binary'],'snapshot']
367
+ if key_comp?(props,'zfsmgmt:snapshot','recursive') and key_comp?(props,'zfsmgmt:snapshot@source',['local','received'])
368
+ com.push('-r')
369
+ end
370
+ com.push("#{zfs}@#{[prefix,dt.strftime(ts)].join('-')}")
371
+ system_com(com,noop)
372
+ end
373
+ end
374
+ def self.system_com(com, noop = false)
375
+ comstr = com.join(' ')
376
+ $logger.info(comstr)
377
+ unless noop
378
+ system(comstr)
379
+ unless $?.success?
380
+ $logger.error("command failed: #{$?.exitstatus}")
381
+ end
382
+ end
383
+ end
384
+ def self.zfs_send(options,zfs,props,snaps)
385
+ sorted = snaps.keys.sort { |a,b| snaps[a]['creation'] <=> snaps[b]['creation'] }
386
+ # compute the zfs "path"
387
+ # ternary operator 4eva
388
+ destination_path = ( options[:destination] ? options[:destination] : props['zfsmgmt:destination'] )
389
+ if props['zfsmgmt:destination@source'] == 'local'
390
+ destination_path = File.join( destination_path,
391
+ File.basename(zfs)
392
+ )
393
+ elsif m = /inherited from (.+)/.match(props['zfsmgmt:destination@source'])
394
+ destination_path = File.join( destination_path,
395
+ File.basename(m[1]),
396
+ zfs.sub(m[1],'')
397
+ )
398
+ else
399
+ $logger.error("fatal error: #{props['zfsmgmt:destination']} source: #{props['zfsmgmt:destination@source']}")
400
+ exit(1)
401
+ end
402
+ # does the destination zfs already exist?
403
+ remote_zfs_state = ''
404
+ begin
405
+ recv_zfs = zfsget(zfs: destination_path,
406
+ command_prefix: recv_command_prefix(options,props),
407
+ #properties: ['receive_resume_token'],
408
+ )
409
+ rescue ZfsGetError
410
+ $logger.debug("recv filesystem doesn't exist: #{destination_path}")
411
+ remote_zfs_state = 'missing'
412
+ else
413
+ if recv_zfs[destination_path].has_key?('receive_resume_token')
414
+ remote_zfs_state = recv_zfs[destination_path]['receive_resume_token']
415
+ else
416
+ remote_zfs_state = 'present'
417
+ end
418
+ end
419
+
420
+ if remote_zfs_state == 'missing'
421
+ # the zfs does not exist, send initial (oldest?) snapshot
422
+ com = []
423
+ source = sorted[0]
424
+ if options[:initial_snapshot] == 'newest' or
425
+ key_comp?(options, 'replicate') or
426
+ key_comp?(props, 'zfsmgmt:send_replcate')
427
+ source = sorted[-1]
428
+ end
429
+ com += zfs_send_com(options,
430
+ props,
431
+ [],
432
+ source,
433
+ )
434
+ e = zfs_send_estimate(com) if options[:verbose] == 'pv'
435
+ com += mbuffer_command(options) if options[:mbuffer]
436
+ com += pv_command(options,e) if options[:verbose] == 'pv'
437
+ com += zfs_recv_com(options,[],props,destination_path)
438
+
439
+ system_com(com)
440
+ unless $?.success?
441
+ return
442
+ end
443
+
444
+ elsif remote_zfs_state != 'present'
445
+ # should be resumable!
446
+ com = [ ]
447
+ com.push( ZfsMgmt.global_options[:zfs_binary], 'send', '-t', remote_zfs_state )
448
+ com.push('-v','-P') if key_comp?(options, 'verbose', 'send')
449
+ com.push('|')
450
+ e = zfs_send_estimate(com) if options[:verbose] == 'pv'
451
+ com += mbuffer_command(options) if options[:mbuffer]
452
+ com += pv_command(options,e) if options[:verbose] == 'pv'
453
+
454
+ recv = [ ZfsMgmt.global_options[:zfs_binary], 'recv', '-s' ]
455
+ recv.push('-n') if options[:noop]
456
+ recv.push('-u') if options[:unmount]
457
+ recv.push('-v') if options[:verbose] and ( options[:verbose] == 'receive' or options[:verbose] == 'recv' )
458
+ recv.push(dq(destination_path))
459
+
460
+ if options[:remote] or props['zfsmgmt:remote']
461
+ if options[:mbuffer]
462
+ recv = mbuffer_command(options) + recv
463
+ end
464
+ recv = recv_command_prefix(options,props) + [ sq(recv.join(' ')) ]
465
+ end
466
+
467
+ com += recv
468
+
469
+ system_com(com)
470
+ unless $?.success?
471
+ return
472
+ end
473
+ end
474
+
475
+ # the zfs already exists, so update with incremental?
476
+ begin
477
+ remote_snaps = zfsget(zfs: destination_path,
478
+ types: ['snapshot'],
479
+ command_prefix: recv_command_prefix(options,props),
480
+ properties: ['creation','userrefs'],
481
+ )
482
+ rescue ZfsGetError
483
+ $logger.error("unable to get remote snapshot information for #{destination_path}")
484
+ return
485
+ end
486
+ unless remote_snaps and remote_snaps.keys.length > 0
487
+ $logger.error("receiving filesystem has NO snapshots, it must be destroyed: #{destination_path}")
488
+ return
489
+ end
490
+ if remote_snaps.has_key?(sorted[-1].sub(zfs,destination_path))
491
+ $logger.info("the most recent local snapshot (#{sorted[-1]}) already exists on the remote side (#{sorted[-1].sub(zfs,destination_path)})")
492
+ return
493
+ end
494
+ remote_snaps.sort_by { |k,v| -v['creation'] }.each do |rsnap,v|
495
+ # oldest first
496
+ #pp rsnap,rsnap.sub(destination_path,zfs)
497
+ #pp snaps
498
+ if snaps.has_key?(rsnap.sub(destination_path,zfs))
499
+ $logger.debug("process #{rsnap} to #{sorted[-1]}")
500
+ com = []
501
+ i_opt = '-i'
502
+ # allow the command line option for intermediary to override the property
503
+ if key_comp?(options,'intermediary',[true,false])
504
+ i_opt = '-I' if key_comp?(options, 'intermediary', true)
505
+ elsif key_comp?(props, 'zfsmgmt:send_intermediary')
506
+ i_opt = '-I'
507
+ end
508
+
509
+ com += zfs_send_com(options,props,[i_opt, dq('@' + rsnap.split('@')[1])], sorted[-1])
510
+ e = zfs_send_estimate(com) if options[:verbose] == 'pv'
511
+ com += mbuffer_command(options) if options[:mbuffer]
512
+ com += pv_command(options,e) if options[:verbose] == 'pv'
513
+ com += zfs_recv_com(options,[],props,destination_path)
514
+
515
+ system_com(com)
516
+ return
517
+ end
518
+ $logger.debug("skipping remote snapshot #{rsnap} because the same snapshot doesn't exist locally #{rsnap.sub(destination_path,zfs)}")
519
+ end
520
+ $logger.error("receiving filesystem has no snapshots that still exists on the sending side, it must be destroyed: #{destination_path}")
521
+
522
+ end
523
+ def self.mbuffer_command(options)
524
+ mbuffer_command = [ ZfsMgmt.global_options[:mbuffer_binary] ]
525
+ mbuffer_command.push('-q') unless options[:verbose] == 'mbuffer'
526
+ mbuffer_command.push('-m',options[:mbuffer_size]) if options[:mbuffer_size]
527
+ mbuffer_command.push('|')
528
+ mbuffer_command
529
+ end
530
+ def self.zfs_send_com(options,props,extra_opts,target)
531
+ zfs_send_com = [ ZfsMgmt.global_options[:zfs_binary], 'send' ]
532
+ zfs_send_com.push('-v','-P') if key_comp?(options,'verbose','send')
533
+ send_opts = {
534
+ 'backup' => '-b',
535
+ 'compressed' => '-c',
536
+ 'embed' => '-e',
537
+ 'holds' => '-h',
538
+ 'large_block' => '-L',
539
+ 'props' => '-p',
540
+ 'raw' => '-w',
541
+ 'replicate' => '-R',
542
+ }
543
+ send_opts.each do |p,o|
544
+ # allow the command line options to override the properties value
545
+ if key_comp?(options,p,[true,false])
546
+ zfs_send_com.push(o) if key_comp?(options,p,true)
547
+ elsif key_comp?(props,"zfsmgmt:send_#{p}")
548
+ zfs_send_com.push(o)
549
+ end
550
+ end
551
+ zfs_send_com + extra_opts + [dq(target),'|']
552
+ end
553
+ def self.zfs_recv_com(options,extra_opts,props,target)
554
+ zfs_recv_com = [ ZfsMgmt.global_options[:zfs_binary], 'recv', '-F', '-s' ]
555
+ recv_opts = {
556
+ 'noop' => '-n',
557
+ 'drop_holds' => '-h',
558
+ 'unmount' => '-u',
559
+ #'discard_last' => '-e',
560
+ #'discard_first' => '-d',
561
+ }
562
+ recv_opts.each do |p,o|
563
+ # allow the command line options to override the properties value
564
+ if key_comp?(options,p,[true,false])
565
+ zfs_recv_com.push(o) if key_comp?(options,p,true)
566
+ elsif key_comp?(props,"zfsmgmt:recv_#{p}")
567
+ zfs_recv_com.push(o)
568
+ end
569
+ end
570
+ zfs_recv_com.push('-v') if key_comp?(options, 'verbose', ['receive', 'recv'])
571
+ if options[:exclude]
572
+ options[:exclude].each do |x|
573
+ zfs_recv_com.push('-x',x)
574
+ end
575
+ end
576
+ if options[:option]
577
+ options[:option].each do |x|
578
+ zfs_recv_com.push('-o',x)
579
+ end
580
+ end
581
+ zfs_recv_com += extra_opts
582
+ zfs_recv_com.push(dq(target))
583
+
584
+ if options[:remote] or props['zfsmgmt:remote']
585
+ if options[:mbuffer]
586
+ zfs_recv_com = mbuffer_command(options) + zfs_recv_com
587
+ end
588
+ zfs_recv_com = recv_command_prefix(options,props) + [ sq(zfs_recv_com.join(' ')) ]
589
+ end
590
+ zfs_recv_com
591
+ end
592
+ def self.recv_command_prefix(options,props)
593
+ ( (options[:remote] or props['zfsmgmt:remote']) ?
594
+ [ 'ssh', ( options[:remote] ? options[:remote] : props['zfsmgmt:remote'] ) ] :
595
+ [] )
596
+ end
597
+ def self.zfs_send_estimate(com)
598
+ lcom = com.dup
599
+ lcom.pop() # remove the pipe symbol
600
+ precom = [ lcom.shift, lcom.shift ]
601
+ lcom.unshift('-P') unless lcom.include?('-P')
602
+ lcom.unshift('-n')
603
+ lcom.push('2>&1')
604
+ lcom = precom + lcom
605
+ $logger.debug(lcom.join(' '))
606
+ %x[#{lcom.join(' ')}].each_line do |l|
607
+ if m = /(incremental|size).*\s+(\d+)$/.match(l)
608
+ return m[2].to_i
609
+ end
610
+ end
611
+ $logger.error("no estimate available")
612
+ return nil
613
+ end
614
+ def self.pv_command(options,estimate)
615
+ a = []
616
+ a += [options[:pv_binary], '-prb' ]
617
+ if estimate
618
+ a += ['-e', '-s', estimate ]
619
+ end
620
+ a.push('|')
621
+ a
622
+ end
623
+
624
+ def self.sq(s)
625
+ "'#{s}'"
626
+ end
627
+ def self.dq(s)
628
+ "\"#{s}\""
629
+ end
630
+ def self.prop_on?(v)
631
+ ['true','on'].include?(v)
632
+ end
633
+ def self.match_filter?(zfs, filter)
634
+ /#{filter}/ =~ zfs
635
+ end
636
+ def self.key_comp?(h,p,v = method(:prop_on?))
637
+ #$logger.debug("p:#{p}\th[p]:#{h[p]}\tv:#{v}")
638
+ return false unless h.has_key?(p)
639
+ if v.kind_of?(Array)
640
+ return v.include?(h[p])
641
+ elsif v.kind_of?(Hash)
642
+ return v.keys.include?(h[p])
643
+ elsif v.kind_of?(Method)
644
+ return v.call(h[p])
645
+ elsif v.kind_of?(Regexp)
646
+ return v =~ h[p]
362
647
  else
648
+ # string, boolean, numbers?
649
+ return h[p] == v
650
+ end
651
+ end
652
+ def self.set_log_level(sev)
653
+ case sev
654
+ when 'debug'
655
+ $logger.level = Logger::DEBUG
656
+ when 'info'
363
657
  $logger.level = Logger::INFO
658
+ when 'warn'
659
+ $logger.level = Logger::WARN
660
+ when 'error'
661
+ $logger.level = Logger::ERROR
662
+ when 'fatal'
663
+ $logger.level = Logger::FATAL
364
664
  end
365
- dt = DateTime.now
366
- zfsget(properties: custom_properties()).each do |zfs,props|
367
- unless /#{filter}/ =~ zfs
665
+ end
666
+ def self.zfs_send_all(options)
667
+ zfs_managed_list(filter: options[:filter],
668
+ property_match: { 'zfsmgmt:send' => method(:prop_on?) }).each do |zfs,props,snaps|
669
+
670
+ if props['zfsmgmt:send@source'] == 'received'
671
+ $logger.debug("skipping received filesystem: #{zfs}")
368
672
  next
369
673
  end
370
- # zfs must have snapshot set to true or recursive
371
- if props.has_key?('zfsmgmt:snapshot') and
372
- props['zfsmgmt:snapshot'] == 'true' or
373
- ( props['zfsmgmt:snapshot'] == 'recursive' and props['zfsmgmt:snapshot@source'] == 'local' ) or
374
- ( props['zfsmgmt:snapshot'] == 'local' and props['zfsmgmt:snapshot@source'] == 'local' )
375
-
376
- prefix = ( props.has_key?('zfsmgmt:snap_prefix') ? props['zfsmgmt:snap_prefix'] : 'zfsmgmt' )
377
- ts = ( props.has_key?('zfsmgmt:snap_timestamp') ? props['zfsmgmt:snap_timestamp'] : '%FT%T%z' )
378
- com = [global_options['zfs_binary'],'snapshot']
379
- if props['zfsmgmt:snapshot'] == 'recursive' and props['zfsmgmt:snapshot@source'] == 'local'
380
- com.push('-r')
381
- end
382
- com.push("#{zfs}@#{[prefix,dt.strftime(ts)].join('-')}")
383
- $logger.info(com)
384
- system(com.join(' '))
674
+ if key_comp?(props,'zfsmgmt:send_replicate') and props['zfsmgmt:send_replicate@source'] != 'local'
675
+ $logger.debug("skipping descendant of replicated filesystems: #{zfs}")
676
+ next
677
+ end
678
+ unless props['zfsmgmt:destination']
679
+ $logger.error("#{zfs}: you must specify a destination zfs path via the user property zfsmgmt:destination, even if using --destination on the command line, skipping")
680
+ next
385
681
  end
682
+ zfs_send(options,zfs,props,snaps)
386
683
  end
387
684
  end
388
685
  end
@@ -18,7 +18,7 @@ module ZfsMgmt::Restic
18
18
  com.push( '--repo', props['zfsmgmt:restic_repository'] )
19
19
  end
20
20
 
21
- $logger.debug("#{com.join(' ')}")
21
+ $logger.info("#{com.join(' ')}")
22
22
  restic_output = %x(#{com.join(' ')})
23
23
  unless $?.success?
24
24
  $logger.error("unable to query the restic database")
@@ -70,7 +70,7 @@ module ZfsMgmt::Restic
70
70
  'zfsmgmt:restic_repository',
71
71
  'userrefs',
72
72
  ],
73
- property_match: { 'zfsmgmt:restic_backup' => 'true' }).each do |blob|
73
+ property_match: { 'zfsmgmt:restic_backup' => ['on','true'] }).each do |blob|
74
74
  zfs,props,zfs_snapshots = blob
75
75
  last_zfs_snapshot = zfs_snapshots.keys.sort { |a,b| zfs_snapshots[a]['creation'] <=> zfs_snapshots[b]['creation'] }.last
76
76
  zfs_snap_time = Time.at(zfs_snapshots[last_zfs_snapshot]['creation'])
@@ -107,7 +107,7 @@ module ZfsMgmt::Restic
107
107
  "zfsmgmt:snapshot=#{last_zfs_snapshot}",
108
108
  "zfsmgmt:zfs=#{zfs}",
109
109
  "zfsmgmt:level=#{level}" ]
110
- com = [ ZfsMgmt.global_options['zfs_binary'], 'send', '-L', '-w', '-h', '-p' ]
110
+ com = [ ZfsMgmt.global_options['zfs_binary'], 'send', '-w', '-h', '-p' ]
111
111
  if level > 0
112
112
  if options[:intermediary]
113
113
  com.push('-I')
@@ -143,8 +143,7 @@ module ZfsMgmt::Restic
143
143
  unless ZfsMgmt.zfs_holds(last_zfs_snapshot).include?('zfsmgmt_restic')
144
144
  ZfsMgmt.zfs_hold('zfsmgmt_restic',last_zfs_snapshot)
145
145
  end
146
- $logger.info("#{com.join(' ')}")
147
- system(com.join(' '))
146
+ ZfsMgmt.system_com(com)
148
147
  chain_snaps = chain.map do |rsnap|
149
148
  rsnap['zfsmgmt:snapshot']
150
149
  end
@@ -1,3 +1,3 @@
1
1
  module ZfsMgmt
2
- VERSION = "0.3.10"
2
+ VERSION = "0.4.4"
3
3
  end
@@ -3,4 +3,5 @@ module ZfsMgmt::ZfsMgr
3
3
  require "zfs_mgmt/zfs_mgr/list"
4
4
  require "zfs_mgmt/zfs_mgr/restic"
5
5
  require "zfs_mgmt/zfs_mgr/snapshot"
6
+ require "zfs_mgmt/zfs_mgr/send"
6
7
  end
@@ -5,21 +5,53 @@ class ZfsMgmt::ZfsMgr::List < Thor
5
5
  :desc => 'only act on zfs matching this regexp'
6
6
  desc "stale", "list all zfs with stale snapshots"
7
7
  method_option :age, :desc => "timeframe outside of which the zfs will be considered stale", :default => '1d'
8
+ method_option :format, :desc => "output format", :type => :string, :enum => ['table','tab'], :default => 'table'
8
9
  def stale()
10
+ ZfsMgmt.set_log_level(options[:loglevel])
9
11
  ZfsMgmt.global_options = options
10
12
  cutoff = Time.at(Time.now.to_i - ZfsMgmt.timespec_to_seconds(options[:age]))
11
13
  table = Text::Table.new
12
14
  table.head = ['zfs','snapshot','age']
13
15
  table.rows = []
14
- ZfsMgmt.zfs_managed_list(filter: options[:filter]).each do |blob|
15
- zfs,props,snaps = blob
16
+ ZfsMgmt.zfs_managed_list(filter: options[:filter]).each do |zfs,props,snaps|
16
17
  last = snaps.keys.sort { |a,b| snaps[a]['creation'] <=> snaps[b]['creation'] }.last
17
18
  snap_time = Time.at(snaps[last]['creation'])
18
19
  if snap_time < cutoff
19
- table.rows << [zfs,last.split('@')[1],snap_time]
20
+ line = [zfs,last.split('@')[1],snap_time]
21
+ table.rows << line
22
+ if options[:format] == 'tab'
23
+ print line.join("\t"),"\n"
24
+ end
20
25
  end
21
26
  end
22
- if table.rows.count > 0
27
+ if options[:format] == 'table' and table.rows.count > 0
28
+ print table.to_s
29
+ end
30
+ end
31
+ desc "holds", "list all holds on snapshots"
32
+ method_option :format, :desc => "output format", :type => :string, :enum => ['table','tab','release'], :default => 'table'
33
+ def holds()
34
+ ZfsMgmt.set_log_level(options[:loglevel])
35
+ ZfsMgmt.global_options = options
36
+ table = Text::Table.new
37
+ table.head = ['snapshot','userrefs','holds']
38
+ table.rows = []
39
+ ZfsMgmt.zfs_managed_list(filter: options[:filter], property_match: {} ).each do |zfs,props,snaps|
40
+ snaps.sort_by { |x,y| y['creation'] }.each do |snap,d|
41
+ if d['userrefs'] > 0
42
+ line = [snap,d['userrefs'].to_s,ZfsMgmt.zfs_holds(snap).join(',')]
43
+ table.rows << line
44
+ if options[:format] == 'tab'
45
+ print line.join("\t"),"\n"
46
+ elsif options[:format] == 'release'
47
+ ZfsMgmt.zfs_holds(snap).each do |hold|
48
+ print "zfs release #{hold} #{snap}\n"
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ if options[:format] == 'table' and table.rows.count > 0
23
55
  print table.to_s
24
56
  end
25
57
  end
@@ -19,17 +19,20 @@ class ZfsMgmt::ZfsMgr::Backup < Thor
19
19
  method_option :level, :desc => "backup level in integer form", :default => 2, :type => :numeric
20
20
  method_option :intermediary, :alias => '-I', :desc => "pass -I (intermediary) option to zfs send", :default => false, :type => :boolean
21
21
  def incremental()
22
+ ZfsMgmt.set_log_level(options[:loglevel])
22
23
  ZfsMgmt.global_options = options
23
24
  ZfsMgmt::Restic.backup(backup_level: options[:level], options: options)
24
25
  end
25
26
  desc "differential", "perform differential backup"
26
27
  method_option :intermediary, :alias => '-I', :desc => "pass -I (intermediary) option to zfs send", :default => false, :type => :boolean
27
28
  def differential()
29
+ ZfsMgmt.set_log_level(options[:loglevel])
28
30
  ZfsMgmt.global_options = options
29
31
  ZfsMgmt::Restic.backup(backup_level: 1, options: options)
30
32
  end
31
33
  desc "full", "perform full backup"
32
34
  def full()
35
+ ZfsMgmt.set_log_level(options[:loglevel])
33
36
  ZfsMgmt.global_options = options
34
37
  ZfsMgmt::Restic.backup(backup_level: 0, options: options)
35
38
  end
@@ -0,0 +1,40 @@
1
+ # zfs send stuff
2
+
3
+ class ZfsMgmt::ZfsMgr::Send < Thor
4
+ class_option :filter, :type => :string, :default => '.+',
5
+ :desc => 'only act on zfs matching this regexp'
6
+ desc "all", "send all zfs configured via user properties"
7
+ method_option :remote, :type => :string,
8
+ :desc => 'remote specification like root@otherhost or localhost'
9
+ method_option :destination, :type => :string,
10
+ :desc => 'destination path like otherpool/ourpool'
11
+ method_option :verbose, :type => :string, :aliases => :'-v', :enum => ['send','receive','recv','mbuffer','pv'],
12
+ :desc => 'enable verbose output on the specified element of the pipe'
13
+ method_option :initial_snapshot, :type => :string, :enum => ['oldest','newest'], :default => 'oldest',
14
+ :desc => 'when sending the initial snapshot use the oldest or most recent snapshot'
15
+
16
+ method_option :intermediary, :aliases => :'-I', :desc => "pass -I option to zfs send", :type => :boolean
17
+ method_option :backup, :aliases => :'-p', :desc => "pass -b (--backup) option to zfs send", :type => :boolean
18
+ method_option :compressed, :aliases => :'-c', :desc => "pass -c (compressed) option to zfs send", :type => :boolean
19
+ method_option :embed, :aliases => :'-e', :desc => "pass -e (--embed) option to zfs send", :type => :boolean
20
+ method_option :holds, :aliases => :'-h', :desc => "pass the -h (--holds) option to zfs send", :type => :boolean
21
+ method_option :large_block, :aliases => :'-L', :desc => "pass -L (--large-block) option to zfs send", :type => :boolean
22
+ method_option :props, :aliases => :'-p', :desc => "pass -p (--props) option to zfs send", :type => :boolean
23
+ method_option :raw, :aliases => :'-w', :desc => "pass -w (--raw) option to zfs send", :type => :boolean
24
+ method_option :replicate, :aliases => :'-R', :desc => "pass -R (--replicate) option to zfs send", :type => :boolean
25
+
26
+ method_option :noop, :aliases => :'-n', :desc => "pass -n (noop) option to zfs send", :type => :boolean
27
+ method_option :unmount, :aliases => :'-u', :desc => "pass -u (unmount) option to zfs receive", :type => :boolean
28
+ method_option :exclude, :aliases => :'-x', :desc => "passed to -x option of receive side", :type => :array
29
+ method_option :option, :aliases => :'-o', :desc => "passed to -o option of receive side", :type => :array
30
+ method_option :drop_holds, :desc => "pass the -h option to zfs recv, indicating holds should be ignored", :type => :boolean
31
+
32
+ method_option :mbuffer, :desc => "insert mbuffer between send and recv", :default => true, :type => :boolean
33
+ method_option :mbuffer_size, :desc => "passed to mbuffer -s option", :type => :string
34
+ def all()
35
+ ZfsMgmt.set_log_level(options[:loglevel])
36
+ ZfsMgmt.global_options = options
37
+
38
+ ZfsMgmt.zfs_send_all(options)
39
+ end
40
+ end
@@ -1,27 +1,30 @@
1
1
  # implement snapshot management
2
2
 
3
3
  class ZfsMgmt::ZfsMgr::Snapshot < Thor
4
- class_option :noop, :type => :boolean, :default => false,
5
- :desc => 'pass -n option to zfs commands'
6
- class_option :verbose, :type => :boolean, :default => false,
7
- :desc => 'pass -v option to zfs commands'
8
- class_option :debug, :type => :boolean, :default => false,
9
- :desc => 'set logging level to debug'
10
4
  class_option :filter, :type => :string, :default => '.+',
11
5
  :desc => 'only act on zfs matching this regexp'
12
6
  desc "destroy", "apply the snapshot destroy policy to zfs"
7
+ method_option :noop, :type => :boolean, :default => false,
8
+ :desc => 'pass -n option to zfs commands'
9
+ method_option :verbose, :type => :boolean, :default => false,
10
+ :desc => 'pass -v option to zfs commands'
13
11
  def destroy()
12
+ ZfsMgmt.set_log_level(options[:loglevel])
14
13
  ZfsMgmt.global_options = options
15
- ZfsMgmt.snapshot_destroy(noop: options[:noop], verbopt: options[:verbose], debugopt: options[:debug], filter: options[:filter])
14
+ ZfsMgmt.snapshot_destroy(noop: options[:noop], verbose: options[:verbose], filter: options[:filter])
16
15
  end
17
16
  desc "policy", "print the policy table for zfs"
18
17
  def policy()
18
+ ZfsMgmt.set_log_level(options[:loglevel])
19
19
  ZfsMgmt.global_options = options
20
- ZfsMgmt.snapshot_policy(verbopt: options[:verbose], debugopt: options[:debug], filter: options[:filter])
20
+ ZfsMgmt.snapshot_policy(filter: options[:filter])
21
21
  end
22
22
  desc "create", "execute zfs snapshot based on zfs properties"
23
+ method_option :noop, :type => :boolean, :default => false,
24
+ :desc => 'log snapshot commands without running zfs snapshot'
23
25
  def create()
26
+ ZfsMgmt.set_log_level(options[:loglevel])
24
27
  ZfsMgmt.global_options = options
25
- ZfsMgmt.snapshot_create(verbopt: options[:verbose], debugopt: options[:debug], filter: options[:filter])
28
+ ZfsMgmt.snapshot_create(noop: options[:noop], filter: options[:filter])
26
29
  end
27
30
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zfs_mgmt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.10
4
+ version: 0.4.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aran Cox
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-02-21 00:00:00.000000000 Z
11
+ date: 2021-03-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -130,6 +130,7 @@ files:
130
130
  - lib/zfs_mgmt/zfs_mgr.rb
131
131
  - lib/zfs_mgmt/zfs_mgr/list.rb
132
132
  - lib/zfs_mgmt/zfs_mgr/restic.rb
133
+ - lib/zfs_mgmt/zfs_mgr/send.rb
133
134
  - lib/zfs_mgmt/zfs_mgr/snapshot.rb
134
135
  - zfs_mgmt.gemspec
135
136
  homepage: https://github.com/aranc23/zfs_mgmt