zfs_mgmt 0.3.10 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a54ed2e0ad255f69367aaeb25fd0dddcc8346cec0fd8be02561e4ea1210510f2
4
- data.tar.gz: 1d31c311f63835c3f7a2b6e771b06628f1901b62130e9ec5d0255ff987fb89bc
3
+ metadata.gz: 96ec8b7a9e8ba4111f9a8e5d58d347de8a62d00c3d5a681f427abcb21e3560a5
4
+ data.tar.gz: 2b11bd3aaa99cc664a3a4308ad33e40a640db631c86490ebb2e571718aa5e5c5
5
5
  SHA512:
6
- metadata.gz: c0534143034e6edb6ce1161bacd86765cf9a151812d1849f3570c983691dfdf8d03f5d0aeefb070fa144fa0fa6838711b855e07180a7c70cd14f3f00d561d562
7
- data.tar.gz: dbf53bcfe6223106f080393621d58ef538a2cf9fe9ae9e7f338b2cbe4e5d88d5bca2fcf44f3c6e8ffb76050ce2d0cfc75fa12638b8d19aae759b9b507fab3a65
6
+ metadata.gz: 0dd229c996513d93439e82e08cfeeefc712202c3af28e72df3256c0a58254f8c525c47e4b1457dc0c50a8d3f3a871cebe9e3c5d9a34974c03adec921e8ed870c
7
+ data.tar.gz: 1a659593c4efa623cb41a8d1586e5d3f34516f960527a1829635b26baad33fcfe8a988585d0be78749692b4b5d6c05cb23dbc4c30be160de8a1c303ed975c13c
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.0)
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' => 'true' } )
239
242
  zfss = [] # array of arrays
240
243
  zfsget(properties: properties).each do |zfs,props|
241
244
  unless /#{filter}/ =~ zfs
@@ -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,309 @@ 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, verbose: false, filter: '.+')
353
+ dt = DateTime.now
354
+ zfsget.select { |zfs,props|
355
+ # must match filter
356
+ match_filter?(zfs: zfs, filter: 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('-v') if verbose
371
+ com.push("#{zfs}@#{[prefix,dt.strftime(ts)].join('-')}")
372
+ system_com(com,noop)
373
+ end
374
+ end
375
+ def self.system_com(com, noop = false)
376
+ comstr = com.join(' ')
377
+ $logger.info(comstr)
378
+ unless noop
379
+ system(comstr)
380
+ unless $?.success?
381
+ $logger.error("command failed: #{$?.exitstatus}")
382
+ end
383
+ end
384
+ end
385
+ def self.zfs_send(options,zfs,props,snaps)
386
+ sorted = snaps.keys.sort { |a,b| snaps[a]['creation'] <=> snaps[b]['creation'] }
387
+ # compute the zfs "path"
388
+ # ternary operator 4eva
389
+ destination_path = ( options[:destination] ? options[:destination] : props['zfsmgmt:destination'] )
390
+ if props['zfsmgmt:destination@source'] == 'local'
391
+ destination_path = File.join( destination_path,
392
+ File.basename(zfs)
393
+ )
394
+ elsif m = /inherited from (.+)/.match(props['zfsmgmt:destination@source'])
395
+ destination_path = File.join( destination_path,
396
+ File.basename(m[1]),
397
+ zfs.sub(m[1],'')
398
+ )
362
399
  else
363
- $logger.level = Logger::INFO
400
+ $logger.error("fatal error: #{props['zfsmgmt:destination']} source: #{props['zfsmgmt:destination@source']}")
401
+ exit(1)
364
402
  end
365
- dt = DateTime.now
366
- zfsget(properties: custom_properties()).each do |zfs,props|
367
- unless /#{filter}/ =~ zfs
368
- next
403
+ # does the destination zfs already exist?
404
+ remote_zfs_state = ''
405
+ begin
406
+ recv_zfs = zfsget(zfs: destination_path,
407
+ command_prefix: recv_command_prefix(options,props),
408
+ #properties: ['receive_resume_token'],
409
+ )
410
+ rescue ZfsGetError
411
+ $logger.debug("recv filesystem doesn't exist: #{destination_path}")
412
+ remote_zfs_state = 'missing'
413
+ else
414
+ if recv_zfs[destination_path].has_key?('receive_resume_token')
415
+ remote_zfs_state = recv_zfs[destination_path]['receive_resume_token']
416
+ else
417
+ remote_zfs_state = 'present'
418
+ end
419
+ end
420
+
421
+ if remote_zfs_state == 'missing'
422
+ # the zfs does not exist, send initial (oldest?) snapshot
423
+ com = []
424
+ source = sorted[0]
425
+ if options[:initial_snapshot] == 'newest' or
426
+ ( options.has_key?('replicate') and options['replicate'] == true ) or
427
+ ( props.has_key?('zfsmgmt:send_replicate') and props['zfsmgmt:send_replicate'] == 'true' )
428
+ source = sorted[-1]
369
429
  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')
430
+ com += zfs_send_com(options,
431
+ props,
432
+ [],
433
+ source,
434
+ )
435
+ e = zfs_send_estimate(com) if options[:verbose] == 'pv'
436
+ com += mbuffer_command(options) if options[:mbuffer]
437
+ com += pv_command(options,e) if options[:verbose] == 'pv'
438
+ com += zfs_recv_com(options,[],props,destination_path)
439
+
440
+ system_com(com)
441
+ unless $?.success?
442
+ return
443
+ end
444
+
445
+ elsif remote_zfs_state != 'present'
446
+ # should be resumable!
447
+ com = [ ]
448
+ com.push( ZfsMgmt.global_options[:zfs_binary], 'send', '-t', remote_zfs_state )
449
+ com.push('-v','-P') if options[:verbose] and options[:verbose] == 'send'
450
+ com.push('|')
451
+ e = zfs_send_estimate(com) if options[:verbose] == 'pv'
452
+ com += mbuffer_command(options) if options[:mbuffer]
453
+ com += pv_command(options,e) if options[:verbose] == 'pv'
454
+
455
+ recv = [ ZfsMgmt.global_options[:zfs_binary], 'recv', '-s' ]
456
+ recv.push('-n') if options[:noop]
457
+ recv.push('-u') if options[:unmount]
458
+ recv.push('-v') if options[:verbose] and ( options[:verbose] == 'receive' or options[:verbose] == 'recv' )
459
+ recv.push(dq(destination_path))
460
+
461
+ if options[:remote] or props['zfsmgmt:remote']
462
+ if options[:mbuffer]
463
+ recv = mbuffer_command(options) + recv
381
464
  end
382
- com.push("#{zfs}@#{[prefix,dt.strftime(ts)].join('-')}")
383
- $logger.info(com)
384
- system(com.join(' '))
465
+ recv = recv_command_prefix(options,props) + [ sq(recv.join(' ')) ]
466
+ end
467
+
468
+ com += recv
469
+
470
+ system_com(com)
471
+ unless $?.success?
472
+ return
473
+ end
474
+ end
475
+
476
+ # the zfs already exists, so update with incremental?
477
+ begin
478
+ remote_snaps = zfsget(zfs: destination_path,
479
+ types: ['snapshot'],
480
+ command_prefix: recv_command_prefix(options,props),
481
+ properties: ['creation','userrefs'],
482
+ )
483
+ rescue ZfsGetError
484
+ $logger.error("unable to get remote snapshot information for #{destination_path}")
485
+ return
486
+ end
487
+ unless remote_snaps and remote_snaps.keys.length > 0
488
+ $logger.error("receiving filesystem has NO snapshots, it must be destroyed: #{destination_path}")
489
+ return
490
+ end
491
+ if remote_snaps.has_key?(sorted[-1].sub(zfs,destination_path))
492
+ $logger.info("the most recent local snapshot (#{sorted[-1]}) already exists on the remote side (#{sorted[-1].sub(zfs,destination_path)})")
493
+ return
494
+ end
495
+ remote_snaps.sort_by { |k,v| -v['creation'] }.each do |rsnap,v|
496
+ # oldest first
497
+ #pp rsnap,rsnap.sub(destination_path,zfs)
498
+ #pp snaps
499
+ if snaps.has_key?(rsnap.sub(destination_path,zfs))
500
+ $logger.debug("process #{rsnap} to #{sorted[-1]}")
501
+ com = []
502
+ com += zfs_send_com(options,props,[(options[:intermediary] ? '-I' : '-i'),dq(rsnap.split('@')[1])],sorted[-1])
503
+ e = zfs_send_estimate(com) if options[:verbose] == 'pv'
504
+ com += mbuffer_command(options) if options[:mbuffer]
505
+ com += pv_command(options,e) if options[:verbose] == 'pv'
506
+ com += zfs_recv_com(options,[],props,destination_path)
507
+
508
+ system_com(com)
509
+ return
510
+ end
511
+ $logger.debug("skipping remote snapshot #{rsnap} because the same snapshot doesn't exist locally #{rsnap.sub(destination_path,zfs)}")
512
+ end
513
+ $logger.error("receiving filesystem has no snapshots that still exists on the sending side, it must be destroyed: #{destination_path}")
514
+
515
+ end
516
+ def self.mbuffer_command(options)
517
+ mbuffer_command = [ ZfsMgmt.global_options[:mbuffer_binary] ]
518
+ mbuffer_command.push('-q') unless options[:verbose] == 'mbuffer'
519
+ mbuffer_command.push('-m',options[:mbuffer_size]) if options[:mbuffer_size]
520
+ mbuffer_command.push('|')
521
+ mbuffer_command
522
+ end
523
+ def self.zfs_send_com(options,props,extra_opts,target)
524
+ zfs_send_com = [ ZfsMgmt.global_options[:zfs_binary], 'send' ]
525
+ zfs_send_com.push('-v','-P') if options[:verbose] and options[:verbose] == 'send'
526
+ send_opts = {
527
+ 'backup' => '-b',
528
+ 'compressed' => '-c',
529
+ 'embed' => '-e',
530
+ 'holds' => '-h',
531
+ 'large_block' => '-L',
532
+ 'props' => '-p',
533
+ 'raw' => '-w',
534
+ 'replicate' => '-R',
535
+ }
536
+ send_opts.each do |p,o|
537
+ if options.has_key?(p)
538
+ zfs_send_com.push(o) if options[p] == true
539
+ elsif props.has_key?("zfsmgmt:send_#{p}")
540
+ zfs_send_com.push(o) if props["zfsmgmt:send_#{p}"] == 'true'
541
+ end
542
+ end
543
+ zfs_send_com + extra_opts + [dq(target),'|']
544
+ end
545
+ def self.zfs_recv_com(options,extra_opts,props,target)
546
+ zfs_recv_com = [ ZfsMgmt.global_options[:zfs_binary], 'recv', '-F', '-s' ]
547
+ recv_opts = {
548
+ 'noop' => '-n',
549
+ 'drop_holds' => '-h',
550
+ 'unmount' => '-u',
551
+ }
552
+ recv_opts.each do |p,o|
553
+ if options.has_key?(p)
554
+ zfs_recv_com.push(o) if options[p] == true
555
+ elsif props.has_key?("zfsmgmt:recv_#{p}")
556
+ zfs_recv_com.push(o) if props["zfsmgmt:recv_#{p}"] == 'true'
557
+ end
558
+ end
559
+ zfs_recv_com.push('-v') if options[:verbose] and ( options[:verbose] == 'receive' or options[:verbose] == 'recv' )
560
+ if options[:exclude]
561
+ options[:exclude].each do |x|
562
+ zfs_recv_com.push('-x',x)
563
+ end
564
+ end
565
+ if options[:option]
566
+ options[:option].each do |x|
567
+ zfs_recv_com.push('-o',x)
385
568
  end
386
569
  end
570
+ zfs_recv_com += extra_opts
571
+ zfs_recv_com.push(dq(target))
572
+
573
+ if options[:remote] or props['zfsmgmt:remote']
574
+ if options[:mbuffer]
575
+ zfs_recv_com = mbuffer_command(options) + zfs_recv_com
576
+ end
577
+ zfs_recv_com = recv_command_prefix(options,props) + [ sq(zfs_recv_com.join(' ')) ]
578
+ end
579
+ zfs_recv_com
580
+ end
581
+ def self.recv_command_prefix(options,props)
582
+ ( (options[:remote] or props['zfsmgmt:remote']) ?
583
+ [ 'ssh', ( options[:remote] ? options[:remote] : props['zfsmgmt:remote'] ) ] :
584
+ [] )
585
+ end
586
+ def self.zfs_send_estimate(com)
587
+ lcom = com.dup
588
+ lcom.pop() # remove the pipe symbol
589
+ precom = [ lcom.shift, lcom.shift ]
590
+ lcom.unshift('-P') unless lcom.include?('-P')
591
+ lcom.unshift('-n')
592
+ lcom.push('2>&1')
593
+ lcom = precom + lcom
594
+ $logger.debug(lcom.join(' '))
595
+ %x[#{lcom.join(' ')}].each_line do |l|
596
+ if m = /(incremental|size).*\s+(\d+)$/.match(l)
597
+ return m[2].to_i
598
+ end
599
+ end
600
+ $logger.error("no estimate available")
601
+ return nil
602
+ end
603
+ def self.pv_command(options,estimate)
604
+ a = []
605
+ a += [options[:pv_binary], '-prb' ]
606
+ if estimate
607
+ a += ['-e', '-s', estimate ]
608
+ end
609
+ a.push('|')
610
+ a
611
+ end
612
+
613
+ def self.sq(s)
614
+ "'#{s}'"
615
+ end
616
+ def self.dq(s)
617
+ "\"#{s}\""
618
+ end
619
+ def self.prop_on?(v)
620
+ ['true','on'].include?(v)
621
+ end
622
+ def self.match_filter?(zfs:, filter:)
623
+ /#{filter}/ =~ zfs
624
+ end
625
+ def self.key_comp?(h,p,v = method(:prop_on?))
626
+ #$logger.debug("p:#{p}\th[p]:#{h[p]}\tv:#{v}")
627
+ return false unless h.has_key?(p)
628
+ if v.kind_of?(Array)
629
+ return v.include?(h[p])
630
+ elsif v.kind_of?(Hash)
631
+ return v.keys.include?(h[p])
632
+ elsif v.kind_of?(String)
633
+ return h[p] == v
634
+ elsif v.kind_of?(Method)
635
+ return v.call(h[p])
636
+ elsif v.kind_of?(Regexp)
637
+ return v =~ h[p]
638
+ else
639
+ raise ArgumentError
640
+ end
641
+ end
642
+ def self.set_log_level(sev)
643
+ case sev
644
+ when 'debug'
645
+ $logger.level = Logger::DEBUG
646
+ when 'info'
647
+ $logger.level = Logger::INFO
648
+ when 'warn'
649
+ $logger.level = Logger::WARN
650
+ when 'error'
651
+ $logger.level = Logger::ERROR
652
+ when 'fatal'
653
+ $logger.level = Logger::FATAL
654
+ end
387
655
  end
388
656
  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")
@@ -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.0"
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,snap|
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,63 @@
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
+ [
39
+ { 'zfsmgmt:send' => 'true' },
40
+ # {
41
+ # 'zfsmgmt:send' => 'replicate',
42
+ # 'zfsmgmt:send@source' => 'local'
43
+ # },
44
+ ].each do |match|
45
+ ZfsMgmt.zfs_managed_list(filter: options[:filter],
46
+ property_match: match).each do |zfs,props,snaps|
47
+ if props['zfsmgmt:send@source'] == 'received'
48
+ $logger.debug("skipping received filesystem: #{zfs}")
49
+ next
50
+ end
51
+ if props.has_key?('zfsmgmt:send_replicate') and props['zfsmgmt:send_replicate'] == 'true' and props['zfsmgmt:send_replicate@source'] != 'local'
52
+ $logger.debug("skipping descendant of replicated filesystems: #{zfs}")
53
+ next
54
+ end
55
+ unless props['zfsmgmt:destination']
56
+ $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")
57
+ next
58
+ end
59
+ ZfsMgmt.zfs_send(options,zfs,props,snaps)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -1,27 +1,32 @@
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'
25
+ method_option :verbose, :type => :boolean, :default => false,
26
+ :desc => 'pass -v option to zfs commands'
23
27
  def create()
28
+ ZfsMgmt.set_log_level(options[:loglevel])
24
29
  ZfsMgmt.global_options = options
25
- ZfsMgmt.snapshot_create(verbopt: options[:verbose], debugopt: options[:debug], filter: options[:filter])
30
+ ZfsMgmt.snapshot_create(noop: options[:noop], verbose: options[:verbose], filter: options[:filter])
26
31
  end
27
32
  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.0
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