zfs_mgmt 0.3.9 → 0.4.3

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: 01dce6574812e84d56d1a59b01e100eab3f8d20cbf424e2536e057ec67a54012
4
- data.tar.gz: 142d2188e242cd55b94c3f5c9375dfa1900ddff8ee608a41d8698d7bcc9bf948
3
+ metadata.gz: 14fbd30f0a3d93dff6442c011b7a613b9639fd5855a9efd635b3b9b14fa336c7
4
+ data.tar.gz: 826699e0d0bca72bf87a779a87b14b9bcc01dffe34596eff471264b07b683d62
5
5
  SHA512:
6
- metadata.gz: 3297d3fb276a460c9b3478316615c34b93ce6a9116d56657e53855327cc0764ddf57cb2c53273bbe72c14624edf1631836378b50ad4b3f62b2fb2a6a21d49970
7
- data.tar.gz: a50bb0e812584aa1b1af794cb729182a5d9d6805e1318c4115462d6718dd2b517c5bd40267c9586cbb5f5ac966bfb742c084576cc3bf6cf9bc4443dd29f3c910
6
+ metadata.gz: fbe275a0b964b664d08346def641abff1c63467e70edb47b60accbd519a3e2df0bf81f2e000f09b28c7cf4db2cd71aa67580c8b570ffc5171835f818b3783d3c
7
+ data.tar.gz: 4b326c73524719f3e103781532228d40fe09f0ebada718701d444d6296dfa5e310e2bd2a4035ad8cb6b08791ff5c8ddf7d0a1ecb6102d305957af853683ba356
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- zfs_mgmt (0.3.9)
4
+ zfs_mgmt (0.4.3)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/bin/zfsmgr CHANGED
@@ -5,10 +5,23 @@ require "zfs_mgmt/zfs_mgr"
5
5
  require "zfs_mgmt/zfs_mgr/restic"
6
6
 
7
7
  class ZfsMgr < Thor
8
+ class_option :zfs_binary, :type => :string, :default => 'zfs',
9
+ :desc => 'zfs binary'
10
+ class_option :zpool_binary, :type => :string, :default => 'zpool',
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'
8
19
  desc "zfsget [ZFS]", "execute zfs get for the given properties and types and parse the output into a nested hash"
9
20
  method_option :properties, :type => :array, :default => ['name'], :desc => "List of properties passed to zfs get"
10
21
  method_option :types, :type => :array, :default => ['filesystem','volume'], enum: ['filesystem','volume','snapshot'], :desc => "list of types"
11
22
  def zfsget(zfs)
23
+ ZfsMgmt.set_log_level(options[:loglevel])
24
+ ZfsMgmt.global_options = options
12
25
  pp ZfsMgmt.zfsget(properties: options[:properties],
13
26
  types: options[:types],
14
27
  zfs: zfs)
@@ -19,6 +32,8 @@ class ZfsMgr < Thor
19
32
  subcommand "list", ZfsMgmt::ZfsMgr::List
20
33
  desc "restic SUBCOMMAND ...ARGS", "backup zfs to restic"
21
34
  subcommand "restic", ZfsMgmt::ZfsMgr::Restic
35
+ desc "send SUBCOMMAND ...ARGS", "send zfs"
36
+ subcommand "send", ZfsMgmt::ZfsMgr::Send
22
37
  end
23
38
 
24
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',
@@ -38,6 +38,11 @@ $properties_xlate = {
38
38
  }
39
39
 
40
40
  module ZfsMgmt
41
+ class << self
42
+ attr_accessor :global_options
43
+ end
44
+ class ZfsGetError < StandardError
45
+ end
41
46
  def self.custom_properties()
42
47
  return [
43
48
  'policy',
@@ -50,6 +55,9 @@ module ZfsMgmt
50
55
  'snapshot',
51
56
  'snap_prefix',
52
57
  'snap_timestamp',
58
+ 'send',
59
+ 'remote',
60
+ 'destination',
53
61
  ].map do |p|
54
62
  ['zfsmgmt',p].join(':')
55
63
  end
@@ -67,7 +75,7 @@ module ZfsMgmt
67
75
  end
68
76
 
69
77
  def self.zfs_holds(snapshot)
70
- com = ['zfs', 'holds', '-H', snapshot]
78
+ com = [global_options['zfs_binary'], 'holds', '-H', snapshot]
71
79
  $logger.debug("#{com.join(' ')}")
72
80
  out = %x(#{com.join(' ')})
73
81
  unless $?.success?
@@ -83,9 +91,8 @@ module ZfsMgmt
83
91
  end
84
92
 
85
93
  def self.zfs_hold(hold,snapshot)
86
- com = ['zfs', 'hold', hold, snapshot]
87
- $logger.debug("#{com.join(' ')}")
88
- system(com.join(' '))
94
+ com = [global_options['zfs_binary'], 'hold', hold, snapshot]
95
+ system_com(com)
89
96
  unless $?.success?
90
97
  errstr = "unable to set hold: #{hold} for snapshot: #{snapshot}"
91
98
  $logger.error(errstr)
@@ -94,9 +101,8 @@ module ZfsMgmt
94
101
  end
95
102
 
96
103
  def self.zfs_release(hold,snapshot)
97
- com = ['zfs', 'release', hold, snapshot]
98
- $logger.debug("#{com.join(' ')}")
99
- system(com.join(' '))
104
+ com = [@global_options['zfs_binary'], 'release', hold, snapshot]
105
+ system_com(com)
100
106
  unless $?.success?
101
107
  errstr = "unable to release hold: #{hold} for snapshot: #{snapshot}"
102
108
  $logger.error(errstr)
@@ -104,19 +110,20 @@ module ZfsMgmt
104
110
  end
105
111
  end
106
112
 
107
- def self.zfsget(properties: ['name'],types: ['filesystem','volume'],zfs: '')
113
+ def self.zfsget(properties: ['all'],types: ['filesystem','volume'],zfs: '', command_prefix: [])
108
114
  results={}
109
- com = ['zfs', 'get', '-Hp', properties.join(','), '-t', types.join(','), zfs]
110
- so,se,status = Open3.capture3(com.join(' '))
115
+ com = [ZfsMgmt.global_options[:zfs_binary], 'get', '-Hp', properties.join(','), '-t', types.join(','), zfs]
116
+ $logger.debug((command_prefix+com).join(' '))
117
+ so,se,status = Open3.capture3((command_prefix+com).join(' '))
111
118
  if status.signaled?
112
119
  $logger.error("process was signalled \"#{com.join(' ')}\", termsig #{status.termsig}")
113
- raise 'ZfsGetError'
120
+ raise ZfsGetError, "process was signalled \"#{com.join(' ')}\", termsig #{status.termsig}"
114
121
  end
115
122
  unless status.success?
116
123
  $logger.error("failed to execute \"#{com.join(' ')}\", exit status #{status.exitstatus}")
117
124
  so.split("\n").each { |l| $logger.debug("stdout: #{l}") }
118
125
  se.split("\n").each { |l| $logger.error("stderr: #{l}") }
119
- raise 'ZfsGetError'
126
+ raise ZfsGetError, "failed to execute \"#{com.join(' ')}\", exit status #{status.exitstatus}"
120
127
  end
121
128
  so.split("\n").each do |line|
122
129
  params = line.split("\t")
@@ -231,7 +238,7 @@ module ZfsMgmt
231
238
  }
232
239
  return saved,saved_snaps,deleteme
233
240
  end
234
- 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' } )
235
242
  zfss = [] # array of arrays
236
243
  zfsget(properties: properties).each do |zfs,props|
237
244
  unless /#{filter}/ =~ zfs
@@ -254,20 +261,20 @@ module ZfsMgmt
254
261
  end
255
262
  return zfss
256
263
  end
257
- def self.snapshot_policy(verbopt: false, debugopt: false, filter: '.+')
258
- if debugopt
259
- $logger.level = Logger::DEBUG
260
- else
261
- $logger.level = Logger::INFO
262
- end
263
- zfs_managed_list(filter: filter).each do |zdata|
264
- (zfs,props,snaps) = zdata
265
- unless props.has_key?('zfsmgmt:policy') and policy_parser(props['zfsmgmt:policy'])
266
- $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")
267
268
  next # zfs
268
269
  end
269
- # call the function that decides who to save and who to delete
270
- (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
271
278
 
272
279
  if saved_snaps.length == 0
273
280
  $logger.info("no snapshots marked as saved by policy for #{zfs}")
@@ -283,21 +290,20 @@ module ZfsMgmt
283
290
  print table.to_s
284
291
  end
285
292
  end
286
- def self.snapshot_destroy(noop: false, verbopt: false, debugopt: false, filter: '.+')
287
- if debugopt
288
- $logger.level = Logger::DEBUG
289
- else
290
- $logger.level = Logger::INFO
291
- end
292
- zfs_managed_list(filter: filter).each do |zdata|
293
- (zfs,props,snaps) = zdata
294
- unless props.has_key?('zfsmgmt:policy') and policy_parser(props['zfsmgmt:policy'])
295
- $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")
296
297
  next # zfs
297
298
  end
298
299
 
299
- # call the function that decides who to save and who to delete
300
- (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
301
307
 
302
308
  $logger.info("deleting #{deleteme.length} snapshots for #{zfs}")
303
309
  deleteme.reverse! # oldest first for removal
@@ -305,32 +311,22 @@ module ZfsMgmt
305
311
  $logger.debug("delete: #{snap_name} #{local_epoch_to_datetime(snaps[snap_name]['creation']).strftime('%F %T')}")
306
312
  end
307
313
 
308
- com_base = "zfs destroy -p"
309
- if deleteme.length > 0
310
- com_base = "#{com_base}d"
311
- end
312
- if noop
313
- com_base = "#{com_base}n"
314
- end
315
- if verbopt
316
- com_base = "#{com_base}v"
317
- 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
318
318
  while deleteme.length > 0
319
319
  for i in 0..(deleteme.length - 1) do
320
320
  max = deleteme.length - 1 - i
321
321
  $logger.debug("attempting to remove snaps 0 through #{max} out of #{deleteme.length} snapshots")
322
322
  bigarg = "#{zfs}@#{deleteme[0..max].map { |s| s.split('@')[1] }.join(',')}"
323
- com = "#{com_base} #{bigarg}"
323
+ com = com_base + [bigarg]
324
324
  $logger.debug("size of bigarg: #{bigarg.length} size of com: #{com.length}")
325
325
  if bigarg.length >= 131072 or com.length >= (2097152-10000)
326
326
  next
327
327
  end
328
- $logger.info(com)
329
328
  deleteme = deleteme - deleteme[0..max]
330
- system(com)
331
- if $?.exitstatus != 0
332
- $logger.error("zfs exited with non-zero status: #{$?.exitstatus}")
333
- end
329
+ system_com(com) # pass -n, always run the command though
334
330
  break
335
331
  end
336
332
  end
@@ -344,7 +340,7 @@ module ZfsMgmt
344
340
  end
345
341
  p = str.scan(/\d+[#{$time_pattern_map.keys.join('')}]/i)
346
342
  unless p.length > 0
347
- raise "unable to parse the policy configuration #{str}"
343
+ raise ArgumentError.new("unable to parse the policy configuration #{str}")
348
344
  end
349
345
  p.each do |pi|
350
346
  scn = /(\d+)([#{$time_pattern_map.keys.join('')}])/i.match(pi)
@@ -352,33 +348,308 @@ module ZfsMgmt
352
348
  end
353
349
  res
354
350
  end
355
- def self.snapshot_create(noop: false, verbopt: false, debugopt: false, filter: '.+')
356
- if debugopt
357
- $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
+ )
358
398
  else
359
- $logger.level = Logger::INFO
399
+ $logger.error("fatal error: #{props['zfsmgmt:destination']} source: #{props['zfsmgmt:destination@source']}")
400
+ exit(1)
360
401
  end
361
- dt = DateTime.now
362
- zfsget(properties: custom_properties()).each do |zfs,props|
363
- unless /#{filter}/ =~ zfs
364
- next
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
+ ( options.has_key?('replicate') and options['replicate'] == true ) or
426
+ ( props.has_key?('zfsmgmt:send_replicate') and props['zfsmgmt:send_replicate'] == 'true' )
427
+ source = sorted[-1]
365
428
  end
366
- # zfs must have snapshot set to true or recursive
367
- if props.has_key?('zfsmgmt:snapshot') and
368
- props['zfsmgmt:snapshot'] == 'true' or
369
- ( props['zfsmgmt:snapshot'] == 'recursive' and props['zfsmgmt:snapshot@source'] == 'local' ) or
370
- ( props['zfsmgmt:snapshot'] == 'local' and props['zfsmgmt:snapshot@source'] == 'local' )
371
-
372
- prefix = ( props.has_key?('zfsmgmt:snap_prefix') ? props['zfsmgmt:snap_prefix'] : 'zfsmgmt' )
373
- ts = ( props.has_key?('zfsmgmt:snap_timestamp') ? props['zfsmgmt:snap_timestamp'] : '%FT%T%z' )
374
- com = ['zfs','snapshot']
375
- if props['zfsmgmt:snapshot'] == 'recursive' and props['zfsmgmt:snapshot@source'] == 'local'
376
- com.push('-r')
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 options[:verbose] and 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
377
463
  end
378
- com.push("#{zfs}@#{[prefix,dt.strftime(ts)].join('-')}")
379
- $logger.info(com)
380
- system(com.join(' '))
464
+ recv = recv_command_prefix(options,props) + [ sq(recv.join(' ')) ]
381
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
+ com += zfs_send_com(options,props,[(options[:intermediary] ? '-I' : '-i'),dq(rsnap.split('@')[1])],sorted[-1])
502
+ e = zfs_send_estimate(com) if options[:verbose] == 'pv'
503
+ com += mbuffer_command(options) if options[:mbuffer]
504
+ com += pv_command(options,e) if options[:verbose] == 'pv'
505
+ com += zfs_recv_com(options,[],props,destination_path)
506
+
507
+ system_com(com)
508
+ return
509
+ end
510
+ $logger.debug("skipping remote snapshot #{rsnap} because the same snapshot doesn't exist locally #{rsnap.sub(destination_path,zfs)}")
511
+ end
512
+ $logger.error("receiving filesystem has no snapshots that still exists on the sending side, it must be destroyed: #{destination_path}")
513
+
514
+ end
515
+ def self.mbuffer_command(options)
516
+ mbuffer_command = [ ZfsMgmt.global_options[:mbuffer_binary] ]
517
+ mbuffer_command.push('-q') unless options[:verbose] == 'mbuffer'
518
+ mbuffer_command.push('-m',options[:mbuffer_size]) if options[:mbuffer_size]
519
+ mbuffer_command.push('|')
520
+ mbuffer_command
521
+ end
522
+ def self.zfs_send_com(options,props,extra_opts,target)
523
+ zfs_send_com = [ ZfsMgmt.global_options[:zfs_binary], 'send' ]
524
+ zfs_send_com.push('-v','-P') if options[:verbose] and options[:verbose] == 'send'
525
+ send_opts = {
526
+ 'backup' => '-b',
527
+ 'compressed' => '-c',
528
+ 'embed' => '-e',
529
+ 'holds' => '-h',
530
+ 'large_block' => '-L',
531
+ 'props' => '-p',
532
+ 'raw' => '-w',
533
+ 'replicate' => '-R',
534
+ }
535
+ send_opts.each do |p,o|
536
+ if options.has_key?(p)
537
+ zfs_send_com.push(o) if options[p] == true
538
+ elsif props.has_key?("zfsmgmt:send_#{p}")
539
+ zfs_send_com.push(o) if props["zfsmgmt:send_#{p}"] == 'true'
540
+ end
541
+ end
542
+ zfs_send_com + extra_opts + [dq(target),'|']
543
+ end
544
+ def self.zfs_recv_com(options,extra_opts,props,target)
545
+ zfs_recv_com = [ ZfsMgmt.global_options[:zfs_binary], 'recv', '-F', '-s' ]
546
+ recv_opts = {
547
+ 'noop' => '-n',
548
+ 'drop_holds' => '-h',
549
+ 'unmount' => '-u',
550
+ }
551
+ recv_opts.each do |p,o|
552
+ if options.has_key?(p)
553
+ zfs_recv_com.push(o) if options[p] == true
554
+ elsif props.has_key?("zfsmgmt:recv_#{p}")
555
+ zfs_recv_com.push(o) if props["zfsmgmt:recv_#{p}"] == 'true'
556
+ end
557
+ end
558
+ zfs_recv_com.push('-v') if options[:verbose] and ( options[:verbose] == 'receive' or options[:verbose] == 'recv' )
559
+ if options[:exclude]
560
+ options[:exclude].each do |x|
561
+ zfs_recv_com.push('-x',x)
562
+ end
563
+ end
564
+ if options[:option]
565
+ options[:option].each do |x|
566
+ zfs_recv_com.push('-o',x)
567
+ end
568
+ end
569
+ zfs_recv_com += extra_opts
570
+ zfs_recv_com.push(dq(target))
571
+
572
+ if options[:remote] or props['zfsmgmt:remote']
573
+ if options[:mbuffer]
574
+ zfs_recv_com = mbuffer_command(options) + zfs_recv_com
575
+ end
576
+ zfs_recv_com = recv_command_prefix(options,props) + [ sq(zfs_recv_com.join(' ')) ]
577
+ end
578
+ zfs_recv_com
579
+ end
580
+ def self.recv_command_prefix(options,props)
581
+ ( (options[:remote] or props['zfsmgmt:remote']) ?
582
+ [ 'ssh', ( options[:remote] ? options[:remote] : props['zfsmgmt:remote'] ) ] :
583
+ [] )
584
+ end
585
+ def self.zfs_send_estimate(com)
586
+ lcom = com.dup
587
+ lcom.pop() # remove the pipe symbol
588
+ precom = [ lcom.shift, lcom.shift ]
589
+ lcom.unshift('-P') unless lcom.include?('-P')
590
+ lcom.unshift('-n')
591
+ lcom.push('2>&1')
592
+ lcom = precom + lcom
593
+ $logger.debug(lcom.join(' '))
594
+ %x[#{lcom.join(' ')}].each_line do |l|
595
+ if m = /(incremental|size).*\s+(\d+)$/.match(l)
596
+ return m[2].to_i
597
+ end
598
+ end
599
+ $logger.error("no estimate available")
600
+ return nil
601
+ end
602
+ def self.pv_command(options,estimate)
603
+ a = []
604
+ a += [options[:pv_binary], '-prb' ]
605
+ if estimate
606
+ a += ['-e', '-s', estimate ]
607
+ end
608
+ a.push('|')
609
+ a
610
+ end
611
+
612
+ def self.sq(s)
613
+ "'#{s}'"
614
+ end
615
+ def self.dq(s)
616
+ "\"#{s}\""
617
+ end
618
+ def self.prop_on?(v)
619
+ ['true','on'].include?(v)
620
+ end
621
+ def self.match_filter?(zfs, filter)
622
+ /#{filter}/ =~ zfs
623
+ end
624
+ def self.key_comp?(h,p,v = method(:prop_on?))
625
+ #$logger.debug("p:#{p}\th[p]:#{h[p]}\tv:#{v}")
626
+ return false unless h.has_key?(p)
627
+ if v.kind_of?(Array)
628
+ return v.include?(h[p])
629
+ elsif v.kind_of?(Hash)
630
+ return v.keys.include?(h[p])
631
+ elsif v.kind_of?(String)
632
+ return h[p] == v
633
+ elsif v.kind_of?(Method)
634
+ return v.call(h[p])
635
+ elsif v.kind_of?(Regexp)
636
+ return v =~ h[p]
637
+ else
638
+ raise ArgumentError
639
+ end
640
+ end
641
+ def self.set_log_level(sev)
642
+ case sev
643
+ when 'debug'
644
+ $logger.level = Logger::DEBUG
645
+ when 'info'
646
+ $logger.level = Logger::INFO
647
+ when 'warn'
648
+ $logger.level = Logger::WARN
649
+ when 'error'
650
+ $logger.level = Logger::ERROR
651
+ when 'fatal'
652
+ $logger.level = Logger::FATAL
382
653
  end
383
654
  end
384
655
  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 = [ 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.9"
2
+ VERSION = "0.4.3"
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,20 +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])
11
+ ZfsMgmt.global_options = options
9
12
  cutoff = Time.at(Time.now.to_i - ZfsMgmt.timespec_to_seconds(options[:age]))
10
13
  table = Text::Table.new
11
14
  table.head = ['zfs','snapshot','age']
12
15
  table.rows = []
13
- ZfsMgmt.zfs_managed_list(filter: options[:filter]).each do |blob|
14
- zfs,props,snaps = blob
16
+ ZfsMgmt.zfs_managed_list(filter: options[:filter]).each do |zfs,props,snaps|
15
17
  last = snaps.keys.sort { |a,b| snaps[a]['creation'] <=> snaps[b]['creation'] }.last
16
18
  snap_time = Time.at(snaps[last]['creation'])
17
19
  if snap_time < cutoff
18
- 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
19
25
  end
20
26
  end
21
- 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
22
55
  print table.to_s
23
56
  end
24
57
  end
@@ -5,8 +5,6 @@ class ZfsMgmt::ZfsMgr::Backup < Thor
5
5
  :desc => 'only act on zfs matching this regexp'
6
6
  class_option :restic_binary, :type => :string, :default => 'restic',
7
7
  :desc => 'restic binary'
8
- class_option :zfs_binary, :type => :string, :default => 'zfs',
9
- :desc => 'zfs binary'
10
8
  class_option :verbose, :alias => '-v', :type => :numeric,
11
9
  :desc => 'verbosity level for restic'
12
10
  class_option :buffer, :type => :string, :default => '256m',
@@ -21,15 +19,21 @@ class ZfsMgmt::ZfsMgr::Backup < Thor
21
19
  method_option :level, :desc => "backup level in integer form", :default => 2, :type => :numeric
22
20
  method_option :intermediary, :alias => '-I', :desc => "pass -I (intermediary) option to zfs send", :default => false, :type => :boolean
23
21
  def incremental()
22
+ ZfsMgmt.set_log_level(options[:loglevel])
23
+ ZfsMgmt.global_options = options
24
24
  ZfsMgmt::Restic.backup(backup_level: options[:level], options: options)
25
25
  end
26
26
  desc "differential", "perform differential backup"
27
27
  method_option :intermediary, :alias => '-I', :desc => "pass -I (intermediary) option to zfs send", :default => false, :type => :boolean
28
28
  def differential()
29
+ ZfsMgmt.set_log_level(options[:loglevel])
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])
36
+ ZfsMgmt.global_options = options
33
37
  ZfsMgmt::Restic.backup(backup_level: 0, options: options)
34
38
  end
35
39
  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,24 +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()
14
- ZfsMgmt.snapshot_destroy(noop: options[:noop], verbopt: options[:verbose], debugopt: options[:debug], filter: options[:filter])
12
+ ZfsMgmt.set_log_level(options[:loglevel])
13
+ ZfsMgmt.global_options = options
14
+ ZfsMgmt.snapshot_destroy(noop: options[:noop], verbose: options[:verbose], filter: options[:filter])
15
15
  end
16
16
  desc "policy", "print the policy table for zfs"
17
17
  def policy()
18
- ZfsMgmt.snapshot_policy(verbopt: options[:verbose], debugopt: options[:debug], filter: options[:filter])
18
+ ZfsMgmt.set_log_level(options[:loglevel])
19
+ ZfsMgmt.global_options = options
20
+ ZfsMgmt.snapshot_policy(filter: options[:filter])
19
21
  end
20
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'
21
25
  def create()
22
- ZfsMgmt.snapshot_create(verbopt: options[:verbose], debugopt: options[:debug], filter: options[:filter])
26
+ ZfsMgmt.set_log_level(options[:loglevel])
27
+ ZfsMgmt.global_options = options
28
+ ZfsMgmt.snapshot_create(noop: options[:noop], filter: options[:filter])
23
29
  end
24
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.9
4
+ version: 0.4.3
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-19 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