zfs-tools 0.2.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.
data/HISTORY ADDED
@@ -0,0 +1,24 @@
1
+ = 0.2.0
2
+
3
+ . first public release
4
+ - zfs_incremental_sync deleted. Not the general solution of the problem we'd
5
+ hoped for.
6
+
7
+ == 0.1.8
8
+
9
+ ! Pool name can now contain spaces.
10
+ * Update to the newest activesupport gem, works with Ruby 1.9.2
11
+ * Dismiss the use of pfexec to run commands that need special privileges, in
12
+ favor of sudo.
13
+ * Reworked tools to use the library instead of issuing commands themselves.
14
+ * [zfs_incremental_sync] Fixed: full synch.
15
+ * [zfs_incremental_sync] Fixed: Missing snapshots on the target
16
+ would be the cause for a failed synch. I hope this is fixed now.
17
+ * [zfs_list_obsolete_snapshots] Works with datasets that have no snapshots
18
+ * [zfs_incremental_sync] Fixed: Bug where sync would abort because there
19
+ were no initial snapshots.
20
+ * zfs_incremental_sync doesn't use sudo anymore. All commands that need
21
+ special privileges are run through pfexec.
22
+ * Now gives incremental sync snapshots special names. This avoids collision
23
+ with the ones done hourly and it also provides information about where
24
+ the snapshot went (to which host).
data/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+
2
+ Copyright (c) 2012,2013 Kaspar Schiess
3
+
4
+ Permission is hereby granted, free of charge, to any person
5
+ obtaining a copy of this software and associated documentation
6
+ files (the "Software"), to deal in the Software without
7
+ restriction, including without limitation the rights to use,
8
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the
10
+ Software is furnished to do so, subject to the following
11
+ conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
20
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
23
+ OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,30 @@
1
+
2
+ A few ZFS tools, mostly related to snapshotting, cleaning up and synching.
3
+
4
+ SYNOPSIS
5
+
6
+ # Snapshot any dataset, snapshot name is the current date/time in a
7
+ # normalized format:
8
+ zfs_snapshot pool1/my/dataset
9
+
10
+ # List snapshots that are obsolete, given some rules about what is
11
+ # considered obsolete:
12
+ echo pool1 | zfs_list_obsolete_snapshots
13
+
14
+ # Safely destroy some dataset (recursively). This will always ask for
15
+ # permission!
16
+ zfs_safe_destroy pool1/my/dataset
17
+
18
+ STATUS
19
+
20
+ This is useful in production; the code needs to be cleaned up.
21
+
22
+ LICENSE
23
+
24
+ MIT license, see LICENSE file.
25
+
26
+ HACKING
27
+
28
+ To run the specs, type `rspec`.
29
+
30
+ Pull requests welcome.
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.dirname(__FILE__) + '/../lib'
4
+ require 'snapshot_set'
5
+ require 'zfs'
6
+
7
+ default = {
8
+ 1.hour => 24,
9
+ 1.day => 7,
10
+ 1.month => 12,
11
+ 1.year => 2
12
+ }
13
+ options = {
14
+ }
15
+
16
+ require 'optparse'
17
+ OptionParser.new { |o|
18
+ o.banner = %Q{Usage: #{$0} [options]
19
+
20
+ Interprets each line on stdin as a zfs dataset. Outputs all snapshots on the
21
+ dataset that are obsolete by the given rule. Example:
22
+
23
+ echo 'pool1/backup' | zfs_list_obsolete_snapshots --daily=3
24
+
25
+ When called without arguments, it will assumes a default of
26
+
27
+ --hourly=24 --daily=7 --monthly=12 --yearly=2
28
+
29
+ }
30
+ o.separator %Q{ Options are:}
31
+
32
+ o.on('--hourly [N]', Integer, "keep N hourly backups (doubles as switch for this help)") do |v|
33
+ if v
34
+ options[1.hour] = v
35
+ else
36
+ puts o
37
+ exit 0
38
+ end
39
+ end
40
+ {
41
+ :daily => 1.day,
42
+ :weekly => 1.week,
43
+ :monthly => 1.month,
44
+ :quarterly => 3.months,
45
+ :semianually => 6.months,
46
+ :yearly => 1.year
47
+ }.each do |name, period|
48
+ o.on("--#{name} N", Integer, "keep N #{name} backups") do |v|
49
+ options[period] = v
50
+ end
51
+ end
52
+ }.parse!(ARGV)
53
+
54
+
55
+ opts = options.empty? ? default : options
56
+ while line=gets
57
+ dataset = ZFS::Dataset.new(line.chomp)
58
+ snapshots = dataset.snapshots
59
+
60
+ keep = SnapshotSet.new(snapshots).gpc(opts).to_a
61
+ remove = (snapshots - keep).sort
62
+
63
+ puts remove.map { |sn| "#{dataset.name}@#{sn}" }
64
+ end
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.dirname(__FILE__) + '/../lib'
4
+
5
+ require 'mixlib/shellout'
6
+
7
+ if ARGV.empty?
8
+ puts %Q{
9
+ Usage: zfs_safe_destroy DATASET
10
+
11
+ Scans the given dataset and prints a summary of what zfs objects
12
+ (filesystems, volumes and snapshots) would be affected if one was to
13
+ recursively destroy the given dataset. If you type 'yes' at the prompt,
14
+ it will then perform a recursive destroy command on the dataset.
15
+
16
+ CAUTION: By typing yes at the prompt, you will loose data. If this is
17
+ your goal, this is your command.
18
+ }
19
+ exit 0
20
+ end
21
+
22
+ def enumerate_objects dataset
23
+ zfs = Mixlib::ShellOut.new(
24
+ 'zfs', 'list -r -t all -Ho name,type ', dataset)
25
+ zfs.run_command
26
+ zfs.error!
27
+
28
+ summary = Hash.new(0)
29
+
30
+ zfs.stdout.lines.map do |line|
31
+ name, _, type = line.chomp.rpartition(' ')
32
+ name.strip!
33
+
34
+ next if name.size == 0
35
+
36
+ summary[type] += 1
37
+ end
38
+
39
+ summary
40
+ rescue => ex
41
+ warn ex.to_s
42
+ warn "Aborting."
43
+ exit(3)
44
+ end
45
+ def zfs_recursive_destroy dataset
46
+ destroy = Mixlib::ShellOut.new('zfs', 'destroy -r ', dataset)
47
+ destroy.run_command
48
+ destroy.error!
49
+
50
+ rescue => ex
51
+ warn ex.to_s
52
+ warn "Aborting."
53
+ exit(4)
54
+ end
55
+
56
+ # ----------------------------------------------------------------- main logic
57
+
58
+ dataset = ARGV.first
59
+ before = enumerate_objects(dataset)
60
+
61
+ puts "You're about to permanently destroy: "
62
+ before.each do |type, count|
63
+ printf "%5d %ss\n", count, type
64
+ end
65
+
66
+ puts "\n'zfs destroy -r #{dataset}'"
67
+ print "Please confirm the operation by typing 'yes': "
68
+ confirmation = $stdin.gets
69
+ exit(1) unless confirmation.chomp == 'yes'
70
+
71
+ after = enumerate_objects(dataset)
72
+ if before != after
73
+ warn "The data that would have been destroyed by the command has changed."
74
+ exit(2)
75
+ end
76
+
77
+ $stdout.sync = true
78
+ print "Destroying..."
79
+ zfs_recursive_destroy dataset
80
+ puts ' Done.'
data/bin/zfs_snapshot ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.dirname(__FILE__) + '/../lib'
4
+ require 'zfs'
5
+
6
+ if ARGV.empty?
7
+ puts %Q{
8
+ Usage: zfs_snapshot DATASET [COMMENT]
9
+
10
+ Snapshots a zfs dataset recursively using a timestamp as snapshot name. If
11
+ you give a comment after the dataset, it will be sanititzed and appended
12
+ to the snapshot name.
13
+
14
+ The name of the created snapshot is output to STDOUT.
15
+ }
16
+ exit 0
17
+ end
18
+
19
+ dataset_name, comment = ARGV.first(2)
20
+
21
+ dataset = ZFS::Dataset.new(dataset_name)
22
+ snapshot_name = dataset.snapshot_with_timestamp(comment)
23
+
24
+ puts snapshot_name
@@ -0,0 +1,113 @@
1
+
2
+ require 'active_support/core_ext/numeric/time'
3
+ require 'active_support/core_ext/integer/time'
4
+ require 'active_support/core_ext/date/calculations'
5
+ require 'time'
6
+ require 'set'
7
+
8
+ # A set of snapshots. This class has methods that allow to determine which
9
+ # snapshots must be deleted in order to clean up space.
10
+ #
11
+ class SnapshotSet
12
+ attr_reader :snapshots
13
+
14
+ # Keeps metadata for each snapshot. Original name is stored as just +name+,
15
+ # extracted timestamp is +time+.
16
+ #
17
+ class Snapshot
18
+ attr_reader :name, :time
19
+
20
+ def initialize(name, default_time=Time.now)
21
+ @time = default_time
22
+ @name = name
23
+ extract_timestamp(name)
24
+ end
25
+
26
+ def extract_timestamp(name)
27
+ if md=name.match(/(\d+)-(\d+)-(\d+)-(\d{2})(\d{2})(\d{2})(.*)/)
28
+ @time = Time.parse(md.captures.join)
29
+ elsif md=name.match(/(\d+)-(\d+)-(\d+)_(\d+)-(\d+)(.*)/)
30
+ @time = Time.parse(md.captures.join)
31
+ else
32
+ @time = Time.parse(name)
33
+ end
34
+ rescue ArgumentError
35
+ # no time information in "foobar"
36
+ return nil
37
+ end
38
+ def to_s
39
+ name
40
+ end
41
+ end
42
+
43
+ # Initialize this class with an unordered set of snapshot names. Names that
44
+ # are of the form 'YYYYMMDDHHMM*' or 'YYYYMMDD*' will be treated as
45
+ # timestamps to which time based rules apply.
46
+ #
47
+ def initialize(snapshot_names)
48
+ @snapshots = snapshot_names.
49
+ map { |name| Snapshot.new(name) }.
50
+ sort_by { |snapshot| snapshot.time }
51
+ end
52
+
53
+ # Returns the size of this set.
54
+ #
55
+ def size
56
+ snapshots.size
57
+ end
58
+
59
+ # Returns the set as an array of snapshot names.
60
+ #
61
+ def to_a
62
+ snapshots.map(&:name)
63
+ end
64
+
65
+ # Computes snapshots to keep according to grandparent-parent-child
66
+ # algorithm. If called with
67
+ #
68
+ # set.gpc(1.day: 3, 1.week: 3)
69
+ #
70
+ # it will return a snapshot set that contains (ideally) 6 snapshots, 3 in
71
+ # the current week starting one day ago, spaced out by one day. The other
72
+ # three will be on week boundaries starting one week ago and going back 3
73
+ # weeks.
74
+ #
75
+ # The algorithm will also return all the snapshots that are less than one
76
+ # day ago.
77
+ #
78
+ def gpc(keep_specification, now=Time.now)
79
+ keep_snapshot_names = Set.new
80
+
81
+ # No snapshots, nothing to keep
82
+ if snapshots.empty?
83
+ return self.class.new([])
84
+ end
85
+
86
+ # Filter snapshots that we need to keep according to keep_specification
87
+ keep_specification.each do |interval, keep_number|
88
+ next if keep_number <= 0
89
+
90
+ # We would like to sample the existing snapshots at regular offsets
91
+ # (n * interval).
92
+ sampling_points = Array(1..keep_number).map { |offset| now - (offset*interval) }
93
+
94
+ # For all sampling points, we'll compute the best snapshot to keep.
95
+ winners = sampling_points.map { |sp|
96
+ snapshots.map { |sh| [(sh.time-sp).abs, sh] }. # <score, snapshot>
97
+ sort_by { |score, sh| score }. # sort by score
98
+ first. # best match
99
+ last # snapshot
100
+ }
101
+
102
+ keep_snapshot_names += winners.map(&:name)
103
+ end
104
+
105
+ # Add snapshots that are within [now, smallest_interval]
106
+ smallest_interval = keep_specification.map { |i,c| i }.min
107
+ keep_snapshot_names += snapshots.
108
+ select { |snapshot| snapshot.time > now-smallest_interval }.
109
+ map(&:name)
110
+
111
+ self.class.new(keep_snapshot_names.to_a)
112
+ end
113
+ end
@@ -0,0 +1,80 @@
1
+
2
+ require 'shellwords'
3
+
4
+ # A zfs dataset such as 'pool1/backup'
5
+ #
6
+ class ZFS::Dataset
7
+ attr_reader :name
8
+ def initialize(name)
9
+ @name = name
10
+ end
11
+
12
+ # Returns an array of snapshots on the dataset. This does not include
13
+ # snapshots on the datasets children.
14
+ #
15
+ def snapshots
16
+ list(name).
17
+ lines.
18
+ select { |l| l.index('@') }. # only snapshots
19
+ map { |l| l.chomp.split('@') }. # <path, snapshot_name>
20
+ select { |path, sn| path==name }. # only direct snapshots
21
+ map { |path,sn| sn } # only snapshot names
22
+ end
23
+
24
+ # Snapshots the dataset with the aid of 'zfs snapshot'. +name+ is sanitized
25
+ # to something zfs accepts, which means that spaces and non-word chars
26
+ # are replaced with '_'.
27
+ #
28
+ def snapshot(name, recursive=false)
29
+ arguments = []
30
+
31
+ snapshot_name = sanitize_snapshot_name(name)
32
+
33
+ arguments << '-r' if recursive
34
+ arguments << "'#{self.name}@#{snapshot_name}'"
35
+
36
+ zfs_snapshot(*arguments)
37
+
38
+ return snapshot_name
39
+ end
40
+
41
+ # Snapshots the dataset with a timestamp that looks like 201101011345 (year,
42
+ # month, day, hour and minute). If a +comment+ is provided, the snapshot
43
+ # will have the comment appended to it, separated by a dash
44
+ # (201101011345-my_example_comment). Note that the sanitizing that #snapshot
45
+ # does applies to the comment as well.
46
+ #
47
+ # This snapshotting is always recursive.
48
+ #
49
+ def snapshot_with_timestamp(comment=nil, time=Time.now)
50
+ name = time.strftime("%Y%m%d%H%M")
51
+ name << "-#{comment}" if comment
52
+
53
+ snapshot(name, true)
54
+ end
55
+
56
+ private
57
+
58
+ # Sanitizes a name to be used as a snapshot name.
59
+ #
60
+ def sanitize_snapshot_name(name)
61
+ name.gsub(/[^-_a-z0-9]/i, '_')
62
+ end
63
+
64
+ # Raw command 'zfs snapshot', args are passed as command line arguments.
65
+ #
66
+ def zfs_snapshot(*args)
67
+ zfs "snapshot", *args
68
+ end
69
+
70
+ # Raw command 'zfs', args are passed as command line arguments.
71
+ #
72
+ def zfs(*args)
73
+ arguments = args.join(' ')
74
+ `sudo /sbin/zfs #{arguments}`
75
+ end
76
+
77
+ def list(dataset)
78
+ zfs 'list', %w(-rH -oname -tall), Shellwords.escape(dataset)
79
+ end
80
+ end
@@ -0,0 +1,29 @@
1
+
2
+
3
+ # Represents a list of snapshots like the tool 'zfs list' outputs it. This
4
+ # class allows extracting information from that list.
5
+ #
6
+ # Example:
7
+ #
8
+ # snl = ZFS::SnapshotList.new(`zfs list -H -o name`)
9
+ # snl.datasets # => array of dataset names
10
+ # snl.snapshots('pool1') # => array of snapshots on the pool1
11
+ #
12
+ class ZFS::SnapshotList
13
+ def initialize(tool_output)
14
+ @list = Hash.new { |h,k| h[k] = [] }
15
+
16
+ tool_output.lines.
17
+ map { |l| l.chomp.strip.split('@').first(2) }. # extracts path/snapshot tuples
18
+ each { |path, snapshot|
19
+ snapshots = @list[path]
20
+ snapshots << snapshot if snapshot }
21
+
22
+ end
23
+ def datasets
24
+ @list.keys
25
+ end
26
+ def snapshots(dataset_name)
27
+ @list[dataset_name]
28
+ end
29
+ end
data/lib/zfs.rb ADDED
@@ -0,0 +1,7 @@
1
+
2
+ # A small collection of zfs classes that rely on zfs utilities.
3
+ #
4
+ module ZFS; end
5
+
6
+ require 'zfs/snapshot_list'
7
+ require 'zfs/dataset'
metadata ADDED
@@ -0,0 +1,106 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: zfs-tools
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Kaspar Schiess
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-05-15 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: mixlib-shellout
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '1.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '1.0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: activesupport
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '3.1'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '3.1'
46
+ - !ruby/object:Gem::Dependency
47
+ name: i18n
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0.6'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0.6'
62
+ description:
63
+ email: kaspar.schiess@absurd.li
64
+ executables:
65
+ - zfs_safe_destroy
66
+ - zfs_list_obsolete_snapshots
67
+ - zfs_snapshot
68
+ extensions: []
69
+ extra_rdoc_files: []
70
+ files:
71
+ - LICENSE
72
+ - HISTORY
73
+ - README
74
+ - lib/snapshot_set.rb
75
+ - lib/zfs/dataset.rb
76
+ - lib/zfs/snapshot_list.rb
77
+ - lib/zfs.rb
78
+ - bin/zfs_list_obsolete_snapshots
79
+ - bin/zfs_safe_destroy
80
+ - bin/zfs_snapshot
81
+ homepage: http://bitbucket.org/kschiess/zfs-tools
82
+ licenses: []
83
+ post_install_message:
84
+ rdoc_options: []
85
+ require_paths:
86
+ - lib
87
+ required_ruby_version: !ruby/object:Gem::Requirement
88
+ none: false
89
+ requirements:
90
+ - - ! '>='
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ! '>='
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ requirements: []
100
+ rubyforge_project:
101
+ rubygems_version: 1.8.25
102
+ signing_key:
103
+ specification_version: 3
104
+ summary: A few ZFS tools, mostly related to snapshotting, cleaning up and synching.
105
+ test_files: []
106
+ has_rdoc: