marius-zsnap 0.1.2

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/INSTALL ADDED
@@ -0,0 +1,39 @@
1
+ FreeBSD Installation
2
+ --------------------
3
+
4
+ FreeBSD users please use the sysutils/zfs-snapshot-mgmt port:
5
+
6
+ cd /usr/ports/sysutils/zfs-snapshot-mgmt
7
+ make install clean
8
+
9
+ and follow the instructions printed after installation.
10
+
11
+ See the manpage for details:
12
+
13
+ man zfs-snapshot-mgmt
14
+
15
+
16
+
17
+ Manual Installation
18
+ -------------------
19
+
20
+ Requirements:
21
+ Ruby
22
+ (/usr/local/bin/ruby, edit the first line of script for a different path)
23
+
24
+ Copy the zfs-snapshot-mgmt script to /usr/local/bin.
25
+ Copy the zfs-snapshot-mgmt.conf.sample file to
26
+ /usr/local/etc/zfs-snapshot-mgmt.conf (this path is hardcoded in the script,
27
+ sorry. You may change it manually (grep for CONFIG_FILE)).
28
+ Copy the zfs-snapshot-mgmt.8 file to man8 directory on your system (usually
29
+ /usr/local/share/man/man8/ or just /usr/share/man/man8/).
30
+
31
+ Read the manual page (man zfs-snapshot-mgmt).
32
+
33
+ Edit the zfs-snapshot-mgmt.conf file.
34
+ Add the script to crontab (usually /etc/crontab but may differ on your system),
35
+ append a line like this:
36
+
37
+ */5 * * * * root /usr/local/bin/zfs-snapshot-mgmt
38
+
39
+ From now on the snapshots will be automatically created.
@@ -0,0 +1,36 @@
1
+ #!/usr/local/bin/ruby -w
2
+ # Copyright (c) 2008, Marcin Simonides
3
+ # Copyright (c) 2009, Marius Nuennerich
4
+ # All rights reserved.
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ # * Redistributions of source code must retain the above copyright
9
+ # notice, this list of conditions and the following disclaimer.
10
+ # * Redistributions in binary form must reproduce the above copyright
11
+ # notice, this list of conditions and the following disclaimer in the
12
+ # documentation and/or other materials provided with the distribution.
13
+ #
14
+ # THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
15
+ # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
16
+ # FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR
17
+ # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
23
+ # POSSIBILITY OF SUCH DAMAGE.
24
+
25
+ require 'zsnap'
26
+
27
+ config = Zsnap::Config.new(YAML::load(File.open(Zsnap::CONFIG_FILE_NAME).read))
28
+
29
+ now_minutes = Time.now.to_i / 60
30
+
31
+ config.filesystems.each do |fs|
32
+ unless config.busy_pools.include? fs.pool
33
+ fs.create_snapshot(now_minutes, config.snapshot_prefix)
34
+ fs.remove_snapshots(now_minutes, config.snapshot_prefix)
35
+ end
36
+ end
@@ -0,0 +1,32 @@
1
+ # Automatic ZFS snapshot management configuration file
2
+ #
3
+ # This is a YAML file (see http://www.yaml.org)
4
+ # Use exactly 2 spaces for each indentation level
5
+ #
6
+ snapshot_prefix: auto-
7
+ filesystems:
8
+ tank/usr/home:
9
+ # Create snapshots recursively for all filesystems mounted under this one
10
+ recursive: true
11
+ # Create snapshots every 10 minutes, starting at midnight
12
+ creation_rule:
13
+ at_multiple: 10
14
+ offset: 0
15
+ # Keep all snapshots for the first 90 minutes,
16
+ # then only those that were created at 30 minute intervals for 12 hours
17
+ # (after snapshot creation),
18
+ # then only those that were created at 3 hour intervals, counting at 2:00
19
+ # (i.e. 2:00, 5:00, 8:00, 11:00, 14:00, 17:00, 20:00, 23:00)
20
+ # for 7 days
21
+ preservation_rules:
22
+ - { for_minutes: 90, at_multiple: 0, offset: 0 }
23
+ - { for_minutes: 720, at_multiple: 30, offset: 0 }
24
+ - { for_minutes: 10080, at_multiple: 180, offset: 120 }
25
+ tank/usr:
26
+ # Create snapshots every 24 hours, starting at 20:00.
27
+ creation_rule:
28
+ at_multiple: 1440
29
+ offset: 1200
30
+ # Keep daily snapshots created at 20:00 (in this case all).
31
+ preservation_rules:
32
+ - { for_minutes: 5760, at_multiple: 1440, offset: 1200 }
@@ -0,0 +1,164 @@
1
+ .Dd June 6, 2008
2
+ .Dt ZFS-SNAPSHOT-MGMT 8
3
+ .Os
4
+ .Sh NAME
5
+ .Nm zfs-snapshot-mgmt
6
+ .Nd automate creation of new and removal of stale ZFS snapshots
7
+ .Sh SYNOPSIS
8
+ .Nm
9
+ .Sh DESCRIPTION
10
+ The utility creates ZFS snapshots and removes snapshots it has created that
11
+ are stale.
12
+ Rules for creating and removing are defined in the configuration file.
13
+ .Pp
14
+ .Nm
15
+ is designed to be run by the
16
+ .Xr cron 8
17
+ utility.
18
+ .Sh CONFIGURATION
19
+ .Nm
20
+ reads settings from the
21
+ .Pa /usr/local/etc/zfs-snapshot-mgmt.conf
22
+ file.
23
+ .Ss General Structure
24
+ The file is in the YAML format: it contains keys and values separated by
25
+ a colon (:). Each value may span multiple lines and be a list or a map with
26
+ key-value pairs. The indentation is always a multiple of two spaces.
27
+ .Pp
28
+ There are two top-level options:
29
+ .Bl -tag -width "1234"
30
+ .It snapshot_prefix
31
+ prefix for snapshots created and recognised by this tool.
32
+ All created snapshots
33
+ will have names consisting of this prefix and current date and time.
34
+ Only snapshots beginning with this prefix will be considered for removal.
35
+ .It filesystems
36
+ the main part of configuration - specifies rules for creating and removing
37
+ snapshots at each filesystem.
38
+ The value for this key contains keys for each filesystem
39
+ (as seen by ZFS, e.g. tank/home)
40
+ with snapshot creation and preservation settings.
41
+ .El
42
+ .Pp
43
+ There is an example configuration in the
44
+ .Sx EXAMPLES
45
+ section.
46
+ .Ss Creation Rules
47
+ For each filesystem there is one snapshot
48
+ .Li creation_rule
49
+ defined.
50
+ It specifies when snapshots for the particular filesystem should be created.
51
+ It has two parameters.
52
+ All values are specified in minutes.
53
+ .Bl -tag -width "1234"
54
+ .It at_multiple
55
+ defines how often should a snapshot be created.
56
+ E.g. a value of 60 means that a snapshot is to be created every hour.
57
+ .It offset
58
+ defines an offset from midnight that is to be applied to the
59
+ .Ix at_multiple
60
+ parameter.
61
+ E.g. specifying a value of 30 means that the first snapshot will be taken 30
62
+ minutes after midnight.
63
+ .El
64
+ .Ss Preservation Rules
65
+ These rules specify which of the created snapshots should be preserved.
66
+ All that do not match the any of the rules and whose names begin with
67
+ .Li snapshot_prefix
68
+ are destroyed.
69
+ .Pp
70
+ Each of the rules has three parameters:
71
+ .Bl -tag -width "1234"
72
+ .It for_minutes
73
+ specifies how long after snapshot creation is the rule applicable.
74
+ .It at_multiple
75
+ analogous to the same parameter for
76
+ .Li creation_rule
77
+ - snapshots whose creation time
78
+ (in minutes since midnight)
79
+ is a multiple of this value are retained.
80
+ .It offset
81
+ analogous to the same parameter for
82
+ .Li creation_rule
83
+ - applies to
84
+ .Li at_multiple .
85
+ .El
86
+ .Sh EXAMPLES
87
+ .Ss Crontab Configuration
88
+ To invoke the program every five minutes you may add the following line to
89
+ the
90
+ .Pa /etc/crontab
91
+ configuration file:
92
+ .Pp
93
+ .D1 */5 * * * * root /usr/local/bin/zfs-snapshot-mgmt
94
+ .Pp
95
+ Bear in mind that this effectively limits the resolution to 5 minutes.
96
+ .Ss zfs-snapshot-mgmt.conf
97
+ Here is an example configuration.
98
+ .Bd -literal -offset indent
99
+ snapshot_prefix: auto-
100
+ filesystems:
101
+ tank/usr/home:
102
+ creation_rule:
103
+ at_multiple: 5
104
+ offset: 0
105
+ preservation_rules:
106
+ - { for_minutes: 90, at_multiple: 0, offset: 0 }
107
+ - { for_minutes: 720, at_multiple: 30, offset: 0 }
108
+ - { for_minutes: 10080, at_multiple: 180, offset: 120 }
109
+ tank/usr:
110
+ recursive: true
111
+ creation_rule:
112
+ at_multiple: 1440
113
+ offset: 1200
114
+ preservation_rules:
115
+ - { for_minutes: 5760, at_multiple: 1440, offset: 1200 }
116
+ .Ed
117
+ .Pp
118
+ which specifies the following settings:
119
+ .Bl -bullet
120
+ .It
121
+ names of created snapshots start with
122
+ .Li auto-
123
+ and only such snapshots are considered for removal.
124
+ .It
125
+ there are two filesystems:
126
+ .Li tank/usr/home
127
+ and
128
+ .Li tank/usr .
129
+ .It
130
+ snapshots for
131
+ .Li tank/usr/home
132
+ are created every five minutes, starting at midnight.
133
+ .It
134
+ all snapshots for
135
+ .Li tank/usr/home
136
+ are kept for 90 minutes after creation.
137
+ .It
138
+ only snapshots created af full and half hour are retained for 12 hours
139
+ (720 minutes) after creation for
140
+ .Li tank/usr/home .
141
+ .It
142
+ snapshots created at multiple of three hours starting with 2 a.m. are retained
143
+ for a week (10080 minutes) after creation for
144
+ .Li tank/usr/home .
145
+ This means: 2 a.m., 5 a.m., 8 a.m. and so on.
146
+ .It
147
+ snapshots on
148
+ .Li tank/usr
149
+ are created once a day at 20 p.m. and retained for 4 days.
150
+ .It
151
+ snapshots are taken recursively on
152
+ .Li tank/usr
153
+ filesystem and all filesystems mounted under it.
154
+ .El
155
+ .Sh FILES
156
+ .Pa /usr/local/etc/zfs-snapshot-mgmt.conf
157
+ The configuration file
158
+ .Sh SEE ALSO
159
+ .Xr cron 8 ,
160
+ .Xr zfs 1M
161
+ .Sh AUTHORS
162
+ .An Marcin Simonides Aq marcin@studio4plus.com
163
+ .Sh BUGS
164
+ There is no way to use alternative path for the configuration file.
data/lib/zsnap.rb ADDED
@@ -0,0 +1,178 @@
1
+ #!/usr/local/bin/ruby -w
2
+ # Copyright (c) 2008, Marcin Simonides
3
+ # Copyright (c) 2009, Marius Nuennerich
4
+ # All rights reserved.
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ # * Redistributions of source code must retain the above copyright
9
+ # notice, this list of conditions and the following disclaimer.
10
+ # * Redistributions in binary form must reproduce the above copyright
11
+ # notice, this list of conditions and the following disclaimer in the
12
+ # documentation and/or other materials provided with the distribution.
13
+ #
14
+ # THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
15
+ # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
16
+ # FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR
17
+ # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
23
+ # POSSIBILITY OF SUCH DAMAGE.
24
+
25
+ require 'yaml'
26
+ require 'time'
27
+
28
+ module Zsnap
29
+
30
+ CONFIG_FILE_NAME = '/usr/local/etc/zfs-snapshot-mgmt.conf'
31
+
32
+ class Rule
33
+ def initialize(args = {})
34
+ args = { 'offset' => 0, 'at_multiple' => 60 }.merge(args)
35
+ @at_multiple = args['at_multiple'].to_i
36
+ @offset = args['offset'].to_i
37
+ end
38
+
39
+ def condition_met?(time_minutes)
40
+ divisor = @at_multiple
41
+ (divisor == 0) or ((time_minutes - @offset) % divisor) == 0
42
+ end
43
+ end
44
+
45
+ class PreservationRule < Rule
46
+ def initialize(args = {})
47
+ super(args)
48
+ args = { 'for_minutes' => 240 }.merge(args)
49
+ @for_minutes = args['for_minutes'].to_i
50
+ end
51
+
52
+ def applies?(now_minutes, creation_time_minutes)
53
+ (now_minutes - creation_time_minutes) < @for_minutes
54
+ end
55
+
56
+ def condition_met_for_snapshot?(now_minutes, snapshot)
57
+ creation_time_minutes = snapshot.creation_time_minutes
58
+ applies?(now_minutes, creation_time_minutes) and
59
+ condition_met?(creation_time_minutes)
60
+ end
61
+ end
62
+
63
+ class SnapshotInfo
64
+ def initialize(name, fs_name, snapshot_prefix)
65
+ @name = name
66
+ @fs_name = fs_name
67
+ @creation_time = parse_timestamp(name[snapshot_prefix.length .. -1])
68
+ end
69
+
70
+ def creation_time_minutes
71
+ @creation_time.to_i / 60
72
+ end
73
+
74
+ # Returns canonical name of the snapshot and FS (as accepted by zfs command)
75
+ # e.g.: /tank/usr@snapshot
76
+ def canonical_name
77
+ if @fs_name and @name
78
+ @fs_name + '@' + @name
79
+ else
80
+ raise "SnapshotInfo doesn't contain name and/or fs_name"
81
+ end
82
+ end
83
+
84
+ private
85
+
86
+ def parse_timestamp(time_string)
87
+ date, time = time_string.split('_')
88
+ year, month, day = date.split('-')
89
+ hour, minute = time.split('.')
90
+ Time.mktime(year, month, day, hour, minute)
91
+ end
92
+ end
93
+
94
+ class FSInfo
95
+ def initialize(fs_name, values)
96
+ @name = fs_name
97
+ raise "Filesystem #{fs_name} has no creation rule" unless values['creation_rule']
98
+ raise "Filesystem #{fs_name} has no preservation rules" unless values['preservation_rules']
99
+
100
+ @creation_rule = Rule.new(values['creation_rule'])
101
+ @preservation_rules = values['preservation_rules'].map do |value|
102
+ PreservationRule.new(value)
103
+ end
104
+ @is_recursive = values['recursive'] ? true : false
105
+ end
106
+
107
+ def create?(now_minutes)
108
+ @creation_rule.condition_met?(now_minutes)
109
+ end
110
+
111
+ def snapshots(prefix)
112
+ path = File.join(mount_point, '.zfs', 'snapshot')
113
+ Dir.open(path).select do |name|
114
+ name[0, prefix.length] == prefix
115
+ end.map { |name| SnapshotInfo.new(name, @name, prefix) }
116
+ end
117
+
118
+ def snapshots_to_remove(now_minutes, prefix)
119
+ snapshots(prefix).reject do |snapshot|
120
+ @preservation_rules.any? do |rule|
121
+ rule.condition_met_for_snapshot?(now_minutes, snapshot)
122
+ end
123
+ end
124
+ end
125
+
126
+ def remove_snapshots(now_minutes, prefix)
127
+ snapshots_to_remove(now_minutes, prefix).each do |snapshot|
128
+ remove_snapshot(snapshot)
129
+ end
130
+ end
131
+
132
+ def create_snapshot(now_minutes, prefix)
133
+ if create?(now_minutes)
134
+ create_snapshot_from_info(SnapshotInfo.new(prefix + Time.now.strftime('%Y-%m-%d_%H.%M'), @name, prefix))
135
+ end
136
+ end
137
+
138
+ def pool
139
+ # More or less according to ZFS Component Naming Requirements
140
+ # http://docs.sun.com/app/docs/doc/819-5461/gbcpt
141
+ @name[/\A[a-zA-Z_:.-]+/]
142
+ end
143
+
144
+ private
145
+
146
+ def remove_snapshot(snapshot_info)
147
+ arguments = @is_recursive ? '-r ' : ''
148
+ system 'zfs destroy ' + arguments + snapshot_info.canonical_name
149
+ end
150
+
151
+ def create_snapshot_from_info(snapshot_info)
152
+ arguments = @is_recursive ? '-r ' : ''
153
+ system 'zfs snapshot ' + arguments + snapshot_info.canonical_name
154
+ end
155
+
156
+ def mount_point
157
+ `zfs mount`.collect { |line| line.split(' ') }.
158
+ select { |item| item.first == @name }.collect { |item| item.last }.first
159
+ end
160
+ end
161
+
162
+ class Config
163
+ attr_reader :snapshot_prefix, :filesystems
164
+
165
+ def initialize(value)
166
+ @snapshot_prefix = value['snapshot_prefix']
167
+ @filesystems = value['filesystems'].map { |key, val| FSInfo.new(key, val) }
168
+ @pools = @filesystems.map { |fs| fs.pool }.uniq
169
+ end
170
+
171
+ def busy_pools
172
+ @busy_pools ||= @pools.select do |pool|
173
+ `zpool status #{pool}`.any? { |line| line =~ /(scrub|resilver) in progress/ }
174
+ end
175
+ end
176
+ end
177
+
178
+ end
@@ -0,0 +1,73 @@
1
+ require 'test/unit'
2
+ require 'lib/zsnap'
3
+
4
+ include Zsnap
5
+
6
+ class RuleTest < Test::Unit::TestCase
7
+ def test_rule
8
+ r = Rule.new
9
+ 2.times do
10
+ assert r.condition_met?(0)
11
+ assert r.condition_met?(60)
12
+ assert r.condition_met?(120)
13
+ (1..59).each do |i|
14
+ assert !r.condition_met?(i)
15
+ end
16
+ r = Rule.new 'at_multiple' => 60
17
+ end
18
+ end
19
+
20
+ def test_rule_with_offset
21
+ r = Rule.new 'at_multiple' => 60, 'offset' => 4
22
+ assert r.condition_met?(4)
23
+ assert r.condition_met?(64)
24
+ assert r.condition_met?(124)
25
+ (5..63).each do |i|
26
+ assert !r.condition_met?(i)
27
+ end
28
+ end
29
+ end
30
+
31
+ class PreservationRuleTest < Test::Unit::TestCase
32
+ def test_create
33
+ p = PreservationRule.new
34
+ assert p.applies?(239, 0)
35
+ assert !p.applies?(240, 0)
36
+ assert !p.applies?(241, 0)
37
+ end
38
+
39
+ def test_condition_met
40
+ p = PreservationRule.new
41
+ s = SnapshotInfo.new 'foo-2009-08-05_21.00', 'bar', 'foo-'
42
+ assert p.condition_met_for_snapshot? 17, s
43
+
44
+ end
45
+ end
46
+
47
+ class SnapshotInfoTest < Test::Unit::TestCase
48
+ def test_create
49
+ s = SnapshotInfo.new 'foo-2009-08-05_21.42', 'bar', 'foo-'
50
+ assert_equal 20825022, s.creation_time_minutes
51
+ assert_equal 'bar@foo-2009-08-05_21.42', s.canonical_name
52
+ end
53
+
54
+ def test_raises_error
55
+ s = SnapshotInfo.new 'foo-2009-08-05_21.42', nil, 'foo-'
56
+ assert_raise(RuntimeError) { s.canonical_name }
57
+ end
58
+ end
59
+
60
+ class FSInfoTest < Test::Unit::TestCase
61
+ def test_create
62
+ rules = {
63
+ 'creation_rule' => {'at_multiple' => 60, 'offset' => 0 },
64
+ 'preservation_rules' => [
65
+ { 'for_minutes' => 240, 'at_multiple' => 0, 'offset' => 0 }
66
+ ]
67
+ }
68
+ f = FSInfo.new 'tank/foo', rules
69
+ assert f.create?(60)
70
+ # Mock here
71
+ # assert_equal 'ff', f.send(:get_mount_point, 'tank/foo')
72
+ end
73
+ end
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: marius-zsnap
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Marcin Simonides
8
+ - Marius Nuennerich
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2009-05-16 00:00:00 -07:00
14
+ default_executable:
15
+ dependencies: []
16
+
17
+ description:
18
+ email: marius@nuenneri.ch
19
+ executables:
20
+ - zfs-snapshot-mgmt
21
+ extensions: []
22
+
23
+ extra_rdoc_files: []
24
+
25
+ files:
26
+ - INSTALL
27
+ - test/zsnap_test.rb
28
+ - lib/zsnap.rb
29
+ - conf/zfs-snapshot-mgmt.conf.sample
30
+ - bin/zfs-snapshot-mgmt
31
+ - doc/zfs-snapshot-mgmt.8
32
+ has_rdoc: false
33
+ homepage: http://github.com/marius/zsnap
34
+ licenses:
35
+ post_install_message:
36
+ rdoc_options: []
37
+
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: "0"
45
+ version:
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: "0"
51
+ version:
52
+ requirements: []
53
+
54
+ rubyforge_project:
55
+ rubygems_version: 1.3.5
56
+ signing_key:
57
+ specification_version: 2
58
+ summary: A script to automatically create and delete zfs snapshots from cron
59
+ test_files:
60
+ - test/zsnap_test.rb