zfs_mgmt 0.3.6 → 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: 1e8c17652083ffa638f535ffc29e7229d2b1a86fa4839772e27c723816825b56
4
- data.tar.gz: 1984744b4a11758b0733d8494145fe896a97da3e89cc93ff29afb9310cbed5b5
3
+ metadata.gz: 96ec8b7a9e8ba4111f9a8e5d58d347de8a62d00c3d5a681f427abcb21e3560a5
4
+ data.tar.gz: 2b11bd3aaa99cc664a3a4308ad33e40a640db631c86490ebb2e571718aa5e5c5
5
5
  SHA512:
6
- metadata.gz: 692848f7054ba6ebe65d2343d3e74e11e4408ff0415bbb55ba127b94a6a97ffc111ec8e455e2d9ae731be73292d1a276f95f5f963b47237e1d8bdd9fe69a1da5
7
- data.tar.gz: ab6171067da56aa19590f9275cd885f4e1beed09bd03916642f9390e5e2409a29979b2c64e9f4339d31594fbbf6a4ee6a841725d6467cd88bb5700130990f168
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.6)
4
+ zfs_mgmt (0.4.0)
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,309 @@ 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, 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
+ )
358
399
  else
359
- $logger.level = Logger::INFO
400
+ $logger.error("fatal error: #{props['zfsmgmt:destination']} source: #{props['zfsmgmt:destination@source']}")
401
+ exit(1)
360
402
  end
361
- dt = DateTime.now
362
- zfsget(properties: custom_properties()).each do |zfs,props|
363
- unless /#{filter}/ =~ zfs
364
- 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]
365
429
  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')
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
377
464
  end
378
- com.push("#{zfs}@#{[prefix,dt.strftime(ts)].join('-')}")
379
- $logger.info(com)
380
- system(com.join(' '))
465
+ recv = recv_command_prefix(options,props) + [ sq(recv.join(' ')) ]
381
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)
568
+ end
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
382
654
  end
383
655
  end
384
656
  end
@@ -9,11 +9,16 @@ module ZfsMgmt::Restic
9
9
  '--tag', 'zfsmgmt',
10
10
  '--path', "/#{zfs}",
11
11
  ]
12
- if props.has_key?('zfsmgmt:restic_repository')
13
- com.push( '-r', props['zfsmgmt:restic_repository'] )
12
+ if options.has_key?('password_file')
13
+ com.push('-p',options['password_file'])
14
14
  end
15
-
16
- $logger.debug("#{com.join(' ')}")
15
+ if options.has_key?('repo')
16
+ com.push('--repo', options['repo'])
17
+ elsif props.has_key?('zfsmgmt:restic_repository')
18
+ com.push( '--repo', props['zfsmgmt:restic_repository'] )
19
+ end
20
+
21
+ $logger.info("#{com.join(' ')}")
17
22
  restic_output = %x(#{com.join(' ')})
18
23
  unless $?.success?
19
24
  $logger.error("unable to query the restic database")
@@ -102,7 +107,7 @@ module ZfsMgmt::Restic
102
107
  "zfsmgmt:snapshot=#{last_zfs_snapshot}",
103
108
  "zfsmgmt:zfs=#{zfs}",
104
109
  "zfsmgmt:level=#{level}" ]
105
- com = [ options[:zfs_binary], 'send', '-L', '-w', '-h', '-p' ]
110
+ com = [ ZfsMgmt.global_options['zfs_binary'], 'send', '-w', '-h', '-p' ]
106
111
  if level > 0
107
112
  if options[:intermediary]
108
113
  com.push('-I')
@@ -119,8 +124,16 @@ module ZfsMgmt::Restic
119
124
  tags.each do |tag|
120
125
  com.push( '--tag', "\"#{tag}\"" )
121
126
  end
122
- if props.has_key?('zfsmgmt:restic_repository')
123
- com.push( '-r', props['zfsmgmt:restic_repository'] )
127
+ if options.has_key?('limit_upload')
128
+ com.push('--limit-upload', options['limit_upload'])
129
+ end
130
+ if options.has_key?('password_file')
131
+ com.push('-p',options['password_file'])
132
+ end
133
+ if options.has_key?('repo')
134
+ com.push('--repo', options['repo'])
135
+ elsif props.has_key?('zfsmgmt:restic_repository')
136
+ com.push( '--repo', props['zfsmgmt:restic_repository'] )
124
137
  end
125
138
  if options[:verbose]
126
139
  com.push('--verbose',options[:verbose])
@@ -130,8 +143,7 @@ module ZfsMgmt::Restic
130
143
  unless ZfsMgmt.zfs_holds(last_zfs_snapshot).include?('zfsmgmt_restic')
131
144
  ZfsMgmt.zfs_hold('zfsmgmt_restic',last_zfs_snapshot)
132
145
  end
133
- $logger.info("#{com.join(' ')}")
134
- system(com.join(' '))
146
+ ZfsMgmt.system_com(com)
135
147
  chain_snaps = chain.map do |rsnap|
136
148
  rsnap['zfsmgmt:snapshot']
137
149
  end
@@ -1,3 +1,3 @@
1
1
  module ZfsMgmt
2
- VERSION = "0.3.6"
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,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,snap|
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,25 +5,35 @@ 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',
13
11
  :desc => 'buffer size for mbuffer'
12
+ class_option :password_file, :alias => '-p', :type => :string,
13
+ :desc => 'passed to restic'
14
+ class_option :limit_upload, :type => :numeric,
15
+ :desc => 'passed to restic'
16
+ class_option :repo, :type => :string,
17
+ :desc => 'passed to restic'
14
18
  desc "incremental", "perform incremental backup"
15
19
  method_option :level, :desc => "backup level in integer form", :default => 2, :type => :numeric
16
20
  method_option :intermediary, :alias => '-I', :desc => "pass -I (intermediary) option to zfs send", :default => false, :type => :boolean
17
21
  def incremental()
22
+ ZfsMgmt.set_log_level(options[:loglevel])
23
+ ZfsMgmt.global_options = options
18
24
  ZfsMgmt::Restic.backup(backup_level: options[:level], options: options)
19
25
  end
20
26
  desc "differential", "perform differential backup"
21
27
  method_option :intermediary, :alias => '-I', :desc => "pass -I (intermediary) option to zfs send", :default => false, :type => :boolean
22
28
  def differential()
29
+ ZfsMgmt.set_log_level(options[:loglevel])
30
+ ZfsMgmt.global_options = options
23
31
  ZfsMgmt::Restic.backup(backup_level: 1, options: options)
24
32
  end
25
33
  desc "full", "perform full backup"
26
34
  def full()
35
+ ZfsMgmt.set_log_level(options[:loglevel])
36
+ ZfsMgmt.global_options = options
27
37
  ZfsMgmt::Restic.backup(backup_level: 0, options: options)
28
38
  end
29
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,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()
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'
25
+ method_option :verbose, :type => :boolean, :default => false,
26
+ :desc => 'pass -v option to zfs commands'
21
27
  def create()
22
- ZfsMgmt.snapshot_create(verbopt: options[:verbose], debugopt: options[:debug], filter: options[:filter])
28
+ ZfsMgmt.set_log_level(options[:loglevel])
29
+ ZfsMgmt.global_options = options
30
+ ZfsMgmt.snapshot_create(noop: options[:noop], verbose: options[:verbose], filter: options[:filter])
23
31
  end
24
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.6
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-18 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