zfstools 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +50 -0
- data/LICENSE.txt +20 -0
- data/README.md +95 -0
- data/README.rdoc +18 -0
- data/Rakefile +53 -0
- data/VERSION +1 -0
- data/bin/zfs-auto-snapshot +47 -0
- data/bin/zfs-cleanup-snapshots +38 -0
- data/bin/zfs-snapshot-mysql +48 -0
- data/features/step_definitions/zfstools_steps.rb +0 -0
- data/features/support/env.rb +13 -0
- data/features/zfstools.feature +9 -0
- data/lib/zfs/snapshot.rb +58 -0
- data/lib/zfstools.rb +172 -0
- data/spec/spec_helper.rb +32 -0
- data/spec/zfstools_spec.rb +7 -0
- metadata +181 -0
data/.document
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
# Add dependencies required to use your gem here.
|
3
|
+
# Example:
|
4
|
+
# gem "activesupport", ">= 2.3.5"
|
5
|
+
|
6
|
+
# Add dependencies to develop your gem here.
|
7
|
+
# Include everything needed to run rake, tests, features, etc.
|
8
|
+
group :development do
|
9
|
+
gem "rspec", "~> 2.8.0"
|
10
|
+
gem "yard", "~> 0.7"
|
11
|
+
gem "rdoc", "~> 3.12"
|
12
|
+
gem "cucumber", ">= 0"
|
13
|
+
gem "bundler", "~> 1.0.0"
|
14
|
+
gem "jeweler", "~> 1.8.3"
|
15
|
+
gem (RUBY_VERSION =~ /^1\.9/ ? "simplecov" : "rcov"), ">= 0"
|
16
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
builder (3.0.0)
|
5
|
+
cucumber (1.1.4)
|
6
|
+
builder (>= 2.1.2)
|
7
|
+
diff-lcs (>= 1.1.2)
|
8
|
+
gherkin (~> 2.7.1)
|
9
|
+
json (>= 1.4.6)
|
10
|
+
term-ansicolor (>= 1.0.6)
|
11
|
+
diff-lcs (1.1.3)
|
12
|
+
gherkin (2.7.7)
|
13
|
+
json (>= 1.4.6)
|
14
|
+
git (1.2.5)
|
15
|
+
jeweler (1.8.3)
|
16
|
+
bundler (~> 1.0)
|
17
|
+
git (>= 1.2.5)
|
18
|
+
rake
|
19
|
+
rdoc
|
20
|
+
json (1.6.5)
|
21
|
+
multi_json (1.0.4)
|
22
|
+
rake (0.9.2.2)
|
23
|
+
rdoc (3.12)
|
24
|
+
json (~> 1.4)
|
25
|
+
rspec (2.8.0)
|
26
|
+
rspec-core (~> 2.8.0)
|
27
|
+
rspec-expectations (~> 2.8.0)
|
28
|
+
rspec-mocks (~> 2.8.0)
|
29
|
+
rspec-core (2.8.0)
|
30
|
+
rspec-expectations (2.8.0)
|
31
|
+
diff-lcs (~> 1.1.2)
|
32
|
+
rspec-mocks (2.8.0)
|
33
|
+
simplecov (0.5.4)
|
34
|
+
multi_json (~> 1.0.3)
|
35
|
+
simplecov-html (~> 0.5.3)
|
36
|
+
simplecov-html (0.5.3)
|
37
|
+
term-ansicolor (1.0.7)
|
38
|
+
yard (0.7.5)
|
39
|
+
|
40
|
+
PLATFORMS
|
41
|
+
ruby
|
42
|
+
|
43
|
+
DEPENDENCIES
|
44
|
+
bundler (~> 1.0.0)
|
45
|
+
cucumber
|
46
|
+
jeweler (~> 1.8.3)
|
47
|
+
rdoc (~> 3.12)
|
48
|
+
rspec (~> 2.8.0)
|
49
|
+
simplecov
|
50
|
+
yard (~> 0.7)
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2012 Bryan Drewery
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
# ZFS Tools
|
2
|
+
|
3
|
+
Various scripts for administrating ZFS. Modeled after [time-sliderd](http://mail.opensolaris.org/pipermail/zfs-discuss/2009-November/033882.html) and [ZFS Automatic Snapshots](https://blogs.oracle.com/timf/entry/zfs_automatic_snapshots_0_12) from OpenSolaris
|
4
|
+
|
5
|
+
## Setup
|
6
|
+
|
7
|
+
Install the gem.
|
8
|
+
|
9
|
+
rake install
|
10
|
+
|
11
|
+
Setup crontab entries for scripts wanted. See below.
|
12
|
+
|
13
|
+
## Scripts
|
14
|
+
|
15
|
+
### zfs-auto-snapshot
|
16
|
+
|
17
|
+
This will handle automatically snapshotting datasets similar to time-sliderd from OpenSolaris. Setup allows you to define your own intervals, snapshot names, and how many to keep for each interval. Zero-sized snapshots will automatically be cleaned up.
|
18
|
+
|
19
|
+
### Usage
|
20
|
+
|
21
|
+
/usr/local/bin/zfs-auto-snapshot INTERVAL KEEP
|
22
|
+
|
23
|
+
* INTERVAL - The interval for the snapshot. This is something such as `frequent`, `hourly`, `daily`, `weekly`, `monthly`, etc.
|
24
|
+
* KEEP - How many to keep for this INTERVAL. Older ones will be destroyed.
|
25
|
+
|
26
|
+
#### Crontab
|
27
|
+
|
28
|
+
15,30,45 * * * * /usr/local/bin/zfs-auto-snapshot frequent 4
|
29
|
+
0 * * * * /usr/local/bin/zfs-auto-snapshot hourly 24
|
30
|
+
7 0 * * * /usr/local/bin/zfs-auto-snapshot daily 7
|
31
|
+
14 0 * * 7 /usr/local/bin/zfs-auto-snapshot weekly 4
|
32
|
+
28 0 1 * * /usr/local/bin/zfs-auto-snapshot monthly 12
|
33
|
+
|
34
|
+
#### Dataset setup
|
35
|
+
|
36
|
+
Only datasets with the `com.sun:auto-snapshot` property set to `true` will be snapshotted.
|
37
|
+
|
38
|
+
zfs set com.sun:auto-snapshot=true DATASET
|
39
|
+
|
40
|
+
##### Overrides
|
41
|
+
|
42
|
+
You can override a child dataset to use, or not use auto snapshotting by settings its flag with the given interval.
|
43
|
+
|
44
|
+
zfs set com.sun:auto-snapshot:weekly=false DATASET
|
45
|
+
|
46
|
+
### zfs-snapshot-mysql
|
47
|
+
|
48
|
+
Snapshots a mysql server's databases. This requires that mysql's `datadir`/`innodb_data_home_dir`/`innodb_log_group_home_dir` be a ZFS dataset.
|
49
|
+
|
50
|
+
#### Example MySQL+ZFS Setup
|
51
|
+
|
52
|
+
##### Datasets
|
53
|
+
|
54
|
+
tank/db/mysql
|
55
|
+
tank/db/mysql/bin-log
|
56
|
+
tank/db/mysql/data
|
57
|
+
tank/db/mysql/innodb
|
58
|
+
tank/db/mysql/innodb/data
|
59
|
+
tank/db/mysql/innodb/log
|
60
|
+
|
61
|
+
##### ZFS Settings
|
62
|
+
|
63
|
+
These settings should be set before importing any data.
|
64
|
+
|
65
|
+
zfs set primarycache=metadata tank/db/mysql/innodb
|
66
|
+
zfs set recordsize=16K tank/db/mysql/innodb/data
|
67
|
+
zfs set recordsize=8K tank/db/mysql/data
|
68
|
+
zfs set compression=lzjb tank/db/mysql/data
|
69
|
+
|
70
|
+
##### MySQL Settings
|
71
|
+
|
72
|
+
innodb_data_home_dir = /tank/db/mysql/innodb/data
|
73
|
+
innodb_log_group_home_dir = /tank/db/mysql/innodb/log
|
74
|
+
datadir = /tank/db/mysql/data
|
75
|
+
log-bin = /tank/db/mysql/bin-log/mysql-bin
|
76
|
+
|
77
|
+
#### Script Usage
|
78
|
+
|
79
|
+
Setup a `/root/.my.cnf` with the relevant information on where to connect to, with the proper username/password that has access to `FLUSH LOGS` and `FLUSH TABLES WITH READ LOCK`.
|
80
|
+
|
81
|
+
#### Crontab
|
82
|
+
|
83
|
+
*/10 * * * * /usr/local/bin/zfs-snapshot-mysql DATASET
|
84
|
+
|
85
|
+
* DATASET - The dataset that contains your mysql data
|
86
|
+
|
87
|
+
### zfs-cleanup-snapshots
|
88
|
+
|
89
|
+
Cleans up zero-sized snapshots. This ignores snapshots created by `zfs-auto-snapshot` as it handles zero-sized in its own special way.
|
90
|
+
|
91
|
+
#### Usage
|
92
|
+
|
93
|
+
#### Crontab
|
94
|
+
|
95
|
+
*/20 * * * * /usr/local/bin/zfs-cleanup-snapshots
|
data/README.rdoc
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
= zfstools
|
2
|
+
|
3
|
+
Various scripts for administrating ZFS. Modeled after OpenSolaris time-slider
|
4
|
+
|
5
|
+
== Contributing to zfstools
|
6
|
+
|
7
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
|
8
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
|
9
|
+
* Fork the project.
|
10
|
+
* Start a feature/bugfix branch.
|
11
|
+
* Commit and push until you are happy with your contribution.
|
12
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
13
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
14
|
+
|
15
|
+
== Copyright
|
16
|
+
|
17
|
+
Copyright (c) 2012 Bryan Drewery. See LICENSE.txt for
|
18
|
+
further details.
|
data/Rakefile
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
|
14
|
+
require 'jeweler'
|
15
|
+
Jeweler::Tasks.new do |gem|
|
16
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
17
|
+
gem.name = "zfstools"
|
18
|
+
gem.homepage = "http://github.com/bdrewery/zfstools"
|
19
|
+
gem.license = "MIT"
|
20
|
+
gem.summary = %Q{ZFSTools}
|
21
|
+
gem.description = %Q{ZFS admin scripts, such as automatic snapshots, mysql snapshotting, scrubbing, etc.}
|
22
|
+
gem.email = "bryan@shatow.net"
|
23
|
+
gem.authors = ["Bryan Drewery"]
|
24
|
+
# dependencies defined in Gemfile
|
25
|
+
end
|
26
|
+
Jeweler::RubygemsDotOrgTasks.new
|
27
|
+
|
28
|
+
require 'rspec/core'
|
29
|
+
require 'rspec/core/rake_task'
|
30
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
31
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
32
|
+
end
|
33
|
+
|
34
|
+
if RUBY_VERSION =~ /^1\.9/
|
35
|
+
desc "Code coverage detail"
|
36
|
+
task :simplecov do
|
37
|
+
ENV['COVERAGE'] = "true"
|
38
|
+
Rake::Task['spec'].execute
|
39
|
+
end
|
40
|
+
else
|
41
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
42
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
43
|
+
spec.rcov = true
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
require 'cucumber/rake/task'
|
48
|
+
Cucumber::Rake::Task.new(:features)
|
49
|
+
|
50
|
+
task :default => :spec
|
51
|
+
|
52
|
+
require 'yard'
|
53
|
+
YARD::Rake::YardocTask.new
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1,47 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
lib_dir = File.join(File.dirname(__FILE__), '..', 'lib')
|
4
|
+
$LOAD_PATH.unshift lib_dir if File.directory?(lib_dir)
|
5
|
+
|
6
|
+
require 'getoptlong'
|
7
|
+
require 'zfstools'
|
8
|
+
|
9
|
+
opts = GetoptLong.new(
|
10
|
+
[ "--utc", "-u", GetoptLong::NO_ARGUMENT ],
|
11
|
+
[ "--dry-run", "-n", GetoptLong::NO_ARGUMENT ]
|
12
|
+
)
|
13
|
+
|
14
|
+
$use_utc = false
|
15
|
+
$dry_run = false
|
16
|
+
opts.each do |opt, arg|
|
17
|
+
case opt
|
18
|
+
when '--utc'
|
19
|
+
$use_utc = true
|
20
|
+
when '--dry-run'
|
21
|
+
$dry_run = true
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
def usage
|
27
|
+
puts <<-EOF
|
28
|
+
Usage: $0 [-un] <INTERVAL> <KEEP>
|
29
|
+
EOF
|
30
|
+
format = " %-15s %s"
|
31
|
+
puts format % ["-u", "Use UTC for snapshots."]
|
32
|
+
puts format % ["-n", "Do a dry-run. Nothing is committed. Only show what would be done."]
|
33
|
+
puts format % ["INTERVAL", "The interval to snapshot."]
|
34
|
+
puts format % ["KEEP", "How many snapshots to keep."]
|
35
|
+
exit
|
36
|
+
end
|
37
|
+
|
38
|
+
usage if ARGV.length < 2
|
39
|
+
|
40
|
+
interval=ARGV[0]
|
41
|
+
keep=ARGV[1].to_i
|
42
|
+
|
43
|
+
# Generate new snapshots
|
44
|
+
do_new_snapshots(interval) if keep > 0
|
45
|
+
|
46
|
+
# Delete expired
|
47
|
+
cleanup_expired_snapshots(interval, keep)
|
@@ -0,0 +1,38 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
#
|
3
|
+
lib_dir = File.join(File.dirname(__FILE__), '..', 'lib')
|
4
|
+
$LOAD_PATH.unshift lib_dir if File.directory?(lib_dir)
|
5
|
+
|
6
|
+
require 'getoptlong'
|
7
|
+
require 'zfstools'
|
8
|
+
|
9
|
+
opts = GetoptLong.new(
|
10
|
+
[ "--dry-run", "-n", GetoptLong::NO_ARGUMENT ]
|
11
|
+
)
|
12
|
+
|
13
|
+
$dry_run = false
|
14
|
+
opts.each do |opt, arg|
|
15
|
+
case opt
|
16
|
+
when '--dry-run'
|
17
|
+
$dry_run = true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
def usage
|
23
|
+
puts <<-EOF
|
24
|
+
Usage: $0 [-n]
|
25
|
+
EOF
|
26
|
+
format = " %-15s %s"
|
27
|
+
puts format % ["-n", "Do a dry-run. Nothing is committed. Only show what would be done."]
|
28
|
+
exit
|
29
|
+
end
|
30
|
+
|
31
|
+
usage if ARGV.length > 0
|
32
|
+
|
33
|
+
snapshots = Zfs::Snapshot.find.select { |snapshot| snapshot.used == 0 and !snapshot.name.include?(snapshot_prefix) }
|
34
|
+
dataset_snapshots = group_snapshots_into_datasets(snapshots)
|
35
|
+
## Group into datasets
|
36
|
+
dataset_snapshots.each do |dataset, snapshots|
|
37
|
+
destroy_zero_sized_snapshots(snapshots)
|
38
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
lib_dir = File.join(File.dirname(__FILE__), '..', 'lib')
|
4
|
+
$LOAD_PATH.unshift lib_dir if File.directory?(lib_dir)
|
5
|
+
|
6
|
+
require 'getoptlong'
|
7
|
+
require 'zfstools'
|
8
|
+
|
9
|
+
opts = GetoptLong.new(
|
10
|
+
[ "--dry-run", "-n", GetoptLong::NO_ARGUMENT ]
|
11
|
+
)
|
12
|
+
|
13
|
+
$dry_run = false
|
14
|
+
opts.each do |opt, arg|
|
15
|
+
case opt
|
16
|
+
when '--dry-run'
|
17
|
+
$dry_run = true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
def usage
|
23
|
+
puts <<-EOF
|
24
|
+
Usage: $0 [-n] DATASET
|
25
|
+
EOF
|
26
|
+
format = " %-15s %s"
|
27
|
+
puts format % ["-n", "Do a dry-run. Nothing is committed. Only show what would be done."]
|
28
|
+
exit
|
29
|
+
end
|
30
|
+
|
31
|
+
usage if ARGV.length < 1
|
32
|
+
|
33
|
+
dataset=ARGV[0]
|
34
|
+
|
35
|
+
snapshot_format = "%Y-%m-%dT%H:%M:%S"
|
36
|
+
snapshot_name = Time.now.strftime(snapshot_format)
|
37
|
+
snapshot_name = "#{dataset}@#{snapshot_name}"
|
38
|
+
|
39
|
+
sql_query=%Q[
|
40
|
+
FLUSH LOGS;
|
41
|
+
FLUSH TABLES WITH READ LOCK;
|
42
|
+
SYSTEM zfs snapshot -r #{snapshot_name};
|
43
|
+
UNLOCK TABLES;
|
44
|
+
]
|
45
|
+
|
46
|
+
cmd = %Q!mysql -e "#{sql_query}"!
|
47
|
+
puts cmd
|
48
|
+
system cmd unless $dry_run
|
File without changes
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
begin
|
3
|
+
Bundler.setup(:default, :development)
|
4
|
+
rescue Bundler::BundlerError => e
|
5
|
+
$stderr.puts e.message
|
6
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
7
|
+
exit e.status_code
|
8
|
+
end
|
9
|
+
|
10
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__) + '/../../lib')
|
11
|
+
require 'zfstools'
|
12
|
+
|
13
|
+
require 'rspec/expectations'
|
data/lib/zfs/snapshot.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
module Zfs
|
2
|
+
class Snapshot
|
3
|
+
@@stale_snapshot_size = false
|
4
|
+
attr_reader :name
|
5
|
+
def initialize(name, used=nil)
|
6
|
+
@name = name
|
7
|
+
@used = used
|
8
|
+
end
|
9
|
+
|
10
|
+
def used
|
11
|
+
if @used.nil? or @@stale_snapshot_size
|
12
|
+
cmd = "zfs get -Hp -o value used #{@name}"
|
13
|
+
@used = %x[#{cmd}].to_i
|
14
|
+
end
|
15
|
+
@used
|
16
|
+
end
|
17
|
+
|
18
|
+
### Find all snapshots in the given interval
|
19
|
+
### @param String match_on The string to match on snapshots
|
20
|
+
def self.find(match_on=nil)
|
21
|
+
snapshots = []
|
22
|
+
cmd = "zfs list -H -t snapshot -o name,used -S name"
|
23
|
+
IO.popen cmd do |io|
|
24
|
+
io.readlines.each do |line|
|
25
|
+
line.chomp!
|
26
|
+
if match_on.nil? or line.include?(match_on)
|
27
|
+
snapshot_name,used = line.split(' ')
|
28
|
+
snapshots << self.new(snapshot_name, used.to_i)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
snapshots
|
33
|
+
end
|
34
|
+
|
35
|
+
### Create a snapshot
|
36
|
+
def self.create(snapshot, options = {})
|
37
|
+
flags=[]
|
38
|
+
flags << "-r" if options['recursive']
|
39
|
+
cmd = "zfs snapshot #{flags.join(" ")} #{snapshot}"
|
40
|
+
puts cmd
|
41
|
+
system(cmd) unless $dry_run
|
42
|
+
end
|
43
|
+
|
44
|
+
### Destroy a snapshot
|
45
|
+
def destroy(options = {})
|
46
|
+
# If destroying a snapshot, need to flag all other snapshot sizes as stale
|
47
|
+
# so they will be relooked up.
|
48
|
+
@@stale_snapshot_size = true
|
49
|
+
# Default to deferred snapshot destroying
|
50
|
+
flags=["-d"]
|
51
|
+
flags << "-r" if options['recursive']
|
52
|
+
cmd = "zfs destroy #{flags.join(" ")} #{@name}"
|
53
|
+
puts cmd
|
54
|
+
system(cmd) unless $dry_run
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
data/lib/zfstools.rb
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require 'zfs/snapshot'
|
4
|
+
|
5
|
+
def snapshot_prefix(interval=nil)
|
6
|
+
prefix = "zfs-auto-snap"
|
7
|
+
if interval
|
8
|
+
prefix += "_#{interval}-"
|
9
|
+
end
|
10
|
+
prefix
|
11
|
+
end
|
12
|
+
|
13
|
+
def snapshot_format
|
14
|
+
'%Y-%m-%d-%Hh%M'
|
15
|
+
end
|
16
|
+
|
17
|
+
### Get the name of the snapshot to create
|
18
|
+
def snapshot_name(interval)
|
19
|
+
if $use_utc
|
20
|
+
date = Time.now.utc.strftime(snapshot_format + "U")
|
21
|
+
else
|
22
|
+
date = Time.now.strftime(snapshot_format)
|
23
|
+
end
|
24
|
+
snapshot_prefix(interval) + date
|
25
|
+
end
|
26
|
+
|
27
|
+
### Find eligible datasets
|
28
|
+
def find_datasets(datasets, property)
|
29
|
+
cmd="zfs list -H -t filesystem,volume -o name,#{property} -s name"
|
30
|
+
all_datasets = datasets['included'] + datasets['excluded']
|
31
|
+
|
32
|
+
IO.popen cmd do |io|
|
33
|
+
io.readlines.each do |line|
|
34
|
+
dataset,value = line.split(" ")
|
35
|
+
# Skip datasets with no value set
|
36
|
+
next if value == "-"
|
37
|
+
# If the dataset is already included/excluded, skip it (for override checking)
|
38
|
+
next if all_datasets.include? dataset
|
39
|
+
if value == "true"
|
40
|
+
datasets['included'] << dataset
|
41
|
+
elsif value == "false"
|
42
|
+
datasets['excluded'] << dataset
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
### Find which datasets can be recursively snapshotted
|
49
|
+
### single snapshot restrictions apply to datasets that have a child in the excluded list
|
50
|
+
def find_recursive_datasets(datasets)
|
51
|
+
all_datasets = datasets['included'] + datasets['excluded']
|
52
|
+
single = []
|
53
|
+
recursive = []
|
54
|
+
cleaned_recursive = []
|
55
|
+
|
56
|
+
### Find datasets that must be single, or are eligible for recursive
|
57
|
+
datasets['included'].each do |dataset|
|
58
|
+
excluded_child = false
|
59
|
+
# Find all children_datasets
|
60
|
+
children_datasets = all_datasets.select { |child_dataset| child_dataset.start_with? dataset }
|
61
|
+
children_datasets.each do |child_dataset|
|
62
|
+
if datasets['excluded'].include?(child_dataset)
|
63
|
+
excluded_child = true
|
64
|
+
single << dataset
|
65
|
+
break
|
66
|
+
end
|
67
|
+
end
|
68
|
+
unless excluded_child
|
69
|
+
recursive << dataset
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
## Cleanup recursive
|
74
|
+
recursive.each do |dataset|
|
75
|
+
if dataset.include?('/')
|
76
|
+
parts = dataset.rpartition('/')
|
77
|
+
parent = parts[0]
|
78
|
+
else
|
79
|
+
parent = dataset
|
80
|
+
end
|
81
|
+
|
82
|
+
# Parent dataset
|
83
|
+
if parent == dataset
|
84
|
+
cleaned_recursive << dataset
|
85
|
+
next
|
86
|
+
end
|
87
|
+
|
88
|
+
# Only add this if its parent is not in the recursive list
|
89
|
+
cleaned_recursive << dataset unless recursive.include?(parent)
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
{ 'single' => single, 'recursive' => cleaned_recursive }
|
94
|
+
end
|
95
|
+
|
96
|
+
### Generate new snapshots
|
97
|
+
def do_new_snapshots(interval)
|
98
|
+
datasets = {
|
99
|
+
'included' => [],
|
100
|
+
'excluded' => [],
|
101
|
+
}
|
102
|
+
|
103
|
+
snapshot_name = snapshot_name(interval)
|
104
|
+
|
105
|
+
# Gather the datasets given the override property
|
106
|
+
find_datasets datasets, "com.sun:auto-snapshot:#{interval}"
|
107
|
+
# Gather all of the datasets without an override
|
108
|
+
find_datasets datasets, "com.sun:auto-snapshot"
|
109
|
+
|
110
|
+
### Determine which datasets can be snapshotted recursively and which not
|
111
|
+
datasets = find_recursive_datasets datasets
|
112
|
+
|
113
|
+
# Snapshot single
|
114
|
+
datasets['single'].each do |dataset|
|
115
|
+
Zfs::Snapshot.create("#{dataset}@#{snapshot_name}")
|
116
|
+
end
|
117
|
+
|
118
|
+
# Snapshot recursive
|
119
|
+
datasets['recursive'].each do |dataset|
|
120
|
+
Zfs::Snapshot.create("#{dataset}@#{snapshot_name}", 'recursive' => true)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def group_snapshots_into_datasets(snapshots)
|
125
|
+
dataset_snapshots = Hash.new {|h,k| h[k] = [] }
|
126
|
+
### Sort into datasets
|
127
|
+
snapshots.each do |snapshot|
|
128
|
+
dataset = snapshot.name.split('@')[0]
|
129
|
+
dataset_snapshots[dataset] << snapshot
|
130
|
+
end
|
131
|
+
dataset_snapshots
|
132
|
+
end
|
133
|
+
|
134
|
+
### Destroy zero-sized snapshots. Recheck after each as the size may have shifted.
|
135
|
+
def destroy_zero_sized_snapshots(snapshots)
|
136
|
+
### Shift off the last, so it maintains the changes
|
137
|
+
saved_snapshot = snapshots.shift(1)
|
138
|
+
remaining_snapshots = [saved_snapshot]
|
139
|
+
snapshots.each do |snapshot|
|
140
|
+
if snapshot.used == 0
|
141
|
+
puts "Destroying zero-sized snapshot: #{snapshot.name}"
|
142
|
+
snapshot.destroy
|
143
|
+
else
|
144
|
+
remaining_snapshots << snapshot
|
145
|
+
end
|
146
|
+
end
|
147
|
+
remaining_snapshots
|
148
|
+
end
|
149
|
+
|
150
|
+
### Find and destroy expired snapshots
|
151
|
+
def cleanup_expired_snapshots(interval, keep)
|
152
|
+
### Find all snapshots matching this interval
|
153
|
+
snapshots = Zfs::Snapshot.find snapshot_prefix(interval)
|
154
|
+
dataset_snapshots = group_snapshots_into_datasets(snapshots)
|
155
|
+
|
156
|
+
### Cleanup zero-sized snapshots before purging old snapshots
|
157
|
+
### Keep the most recent one of the zeros and restore it for the later expired purging
|
158
|
+
dataset_snapshots.each do |dataset, snapshots|
|
159
|
+
## Delete all of the remaining zero-sized snapshots
|
160
|
+
dataset_snapshots[dataset] = destroy_zero_sized_snapshots(snapshots)
|
161
|
+
end
|
162
|
+
|
163
|
+
### Now that zero-sized are removed, remove expired snapshots
|
164
|
+
dataset_snapshots.each do |dataset, snapshots|
|
165
|
+
# Want to keep the first 'keep' entries, so slice them off ...
|
166
|
+
dataset_snapshots[dataset].shift(keep)
|
167
|
+
# ... Now the list only contains snapshots eligible to be destroyed.
|
168
|
+
end
|
169
|
+
dataset_snapshots.values.flatten.each do |snapshot|
|
170
|
+
snapshot.destroy
|
171
|
+
end
|
172
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
3
|
+
|
4
|
+
if RUBY_VERSION =~ /^1\.9/
|
5
|
+
require 'simplecov'
|
6
|
+
|
7
|
+
module SimpleCov::Configuration
|
8
|
+
def clean_filters
|
9
|
+
@filters = []
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
SimpleCov.configure do
|
14
|
+
clean_filters
|
15
|
+
load_adapter 'test_frameworks'
|
16
|
+
end
|
17
|
+
|
18
|
+
ENV["COVERAGE"] && SimpleCov.start do
|
19
|
+
add_filter "/.rvm/"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
require 'rspec'
|
24
|
+
require 'zfstools'
|
25
|
+
|
26
|
+
# Requires supporting files with custom matchers and macros, etc,
|
27
|
+
# in ./support/ and its subdirectories.
|
28
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
29
|
+
|
30
|
+
RSpec.configure do |config|
|
31
|
+
|
32
|
+
end
|
metadata
ADDED
@@ -0,0 +1,181 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: zfstools
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
prerelease: false
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Bryan Drewery
|
13
|
+
autorequire: !!null
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
date: 2012-02-15 00:00:00.000000000 -06:00
|
17
|
+
default_executable: !!null
|
18
|
+
dependencies:
|
19
|
+
- !ruby/object:Gem::Dependency
|
20
|
+
name: rspec
|
21
|
+
requirement: &14899340 !ruby/object:Gem::Requirement
|
22
|
+
none: false
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 2.8.0
|
27
|
+
segments:
|
28
|
+
- 2
|
29
|
+
- 8
|
30
|
+
- 0
|
31
|
+
type: :development
|
32
|
+
prerelease: false
|
33
|
+
version_requirements: *14899340
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: yard
|
36
|
+
requirement: &14896780 !ruby/object:Gem::Requirement
|
37
|
+
none: false
|
38
|
+
requirements:
|
39
|
+
- - ~>
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0.7'
|
42
|
+
segments:
|
43
|
+
- 0
|
44
|
+
- 7
|
45
|
+
type: :development
|
46
|
+
prerelease: false
|
47
|
+
version_requirements: *14896780
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: rdoc
|
50
|
+
requirement: &14895400 !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ~>
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '3.12'
|
56
|
+
segments:
|
57
|
+
- 3
|
58
|
+
- 12
|
59
|
+
type: :development
|
60
|
+
prerelease: false
|
61
|
+
version_requirements: *14895400
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: cucumber
|
64
|
+
requirement: &14894220 !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
segments:
|
71
|
+
- 0
|
72
|
+
type: :development
|
73
|
+
prerelease: false
|
74
|
+
version_requirements: *14894220
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: bundler
|
77
|
+
requirement: &14893080 !ruby/object:Gem::Requirement
|
78
|
+
none: false
|
79
|
+
requirements:
|
80
|
+
- - ~>
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 1.0.0
|
83
|
+
segments:
|
84
|
+
- 1
|
85
|
+
- 0
|
86
|
+
- 0
|
87
|
+
type: :development
|
88
|
+
prerelease: false
|
89
|
+
version_requirements: *14893080
|
90
|
+
- !ruby/object:Gem::Dependency
|
91
|
+
name: jeweler
|
92
|
+
requirement: &14891840 !ruby/object:Gem::Requirement
|
93
|
+
none: false
|
94
|
+
requirements:
|
95
|
+
- - ~>
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: 1.8.3
|
98
|
+
segments:
|
99
|
+
- 1
|
100
|
+
- 8
|
101
|
+
- 3
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: *14891840
|
105
|
+
- !ruby/object:Gem::Dependency
|
106
|
+
name: simplecov
|
107
|
+
requirement: &14865880 !ruby/object:Gem::Requirement
|
108
|
+
none: false
|
109
|
+
requirements:
|
110
|
+
- - ! '>='
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '0'
|
113
|
+
segments:
|
114
|
+
- 0
|
115
|
+
type: :development
|
116
|
+
prerelease: false
|
117
|
+
version_requirements: *14865880
|
118
|
+
description: ZFS admin scripts, such as automatic snapshots, mysql snapshotting, scrubbing,
|
119
|
+
etc.
|
120
|
+
email: bryan@shatow.net
|
121
|
+
executables:
|
122
|
+
- zfs-auto-snapshot
|
123
|
+
- zfs-cleanup-snapshots
|
124
|
+
- zfs-snapshot-mysql
|
125
|
+
extensions: []
|
126
|
+
extra_rdoc_files:
|
127
|
+
- LICENSE.txt
|
128
|
+
- README.md
|
129
|
+
- README.rdoc
|
130
|
+
files:
|
131
|
+
- .document
|
132
|
+
- .rspec
|
133
|
+
- Gemfile
|
134
|
+
- Gemfile.lock
|
135
|
+
- LICENSE.txt
|
136
|
+
- README.md
|
137
|
+
- README.rdoc
|
138
|
+
- Rakefile
|
139
|
+
- VERSION
|
140
|
+
- bin/zfs-auto-snapshot
|
141
|
+
- bin/zfs-cleanup-snapshots
|
142
|
+
- bin/zfs-snapshot-mysql
|
143
|
+
- features/step_definitions/zfstools_steps.rb
|
144
|
+
- features/support/env.rb
|
145
|
+
- features/zfstools.feature
|
146
|
+
- lib/zfs/snapshot.rb
|
147
|
+
- lib/zfstools.rb
|
148
|
+
- spec/spec_helper.rb
|
149
|
+
- spec/zfstools_spec.rb
|
150
|
+
has_rdoc: true
|
151
|
+
homepage: http://github.com/bdrewery/zfstools
|
152
|
+
licenses:
|
153
|
+
- MIT
|
154
|
+
post_install_message: !!null
|
155
|
+
rdoc_options: []
|
156
|
+
require_paths:
|
157
|
+
- lib
|
158
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
159
|
+
none: false
|
160
|
+
requirements:
|
161
|
+
- - ! '>='
|
162
|
+
- !ruby/object:Gem::Version
|
163
|
+
version: '0'
|
164
|
+
segments:
|
165
|
+
- 0
|
166
|
+
hash: 1580374833721105612
|
167
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
168
|
+
none: false
|
169
|
+
requirements:
|
170
|
+
- - ! '>='
|
171
|
+
- !ruby/object:Gem::Version
|
172
|
+
version: '0'
|
173
|
+
segments:
|
174
|
+
- 0
|
175
|
+
requirements: []
|
176
|
+
rubyforge_project: !!null
|
177
|
+
rubygems_version: 1.3.7
|
178
|
+
signing_key: !!null
|
179
|
+
specification_version: 3
|
180
|
+
summary: ZFSTools
|
181
|
+
test_files: []
|