zfs_mgmt 0.3.10 → 0.4.0

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: 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