zfs-tools 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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: