zfs 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +3 -0
- data/Guardfile +11 -0
- data/LICENSE +7 -0
- data/README.md +94 -0
- data/Vagrantfile +38 -0
- data/lib/zfs.rb +513 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/zfs_spec.rb +518 -0
- metadata +112 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# -*- mode: ruby; tab-width: 2; indent-tabs-mode: nil -*-
|
2
|
+
|
3
|
+
guard 'rspec', :version => 2, :cli => '--color' do
|
4
|
+
watch(/^spec\/(.*)_spec.rb/)
|
5
|
+
watch(/^lib\/(.*)\.rb/) { |m| "spec/#{m[1]}_spec.rb" }
|
6
|
+
watch(/^spec\/spec_helper.rb/) { "spec" }
|
7
|
+
end
|
8
|
+
|
9
|
+
guard 'bundler' do
|
10
|
+
watch('Gemfile')
|
11
|
+
end
|
data/LICENSE
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright (c) 2012 Kenneth Vestergaard
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
# ruby-zfs
|
2
|
+
|
3
|
+
A library for interacting with ZFS, made in the spirit of Pathname.
|
4
|
+
|
5
|
+
Just like Pathname, it does not represent the filesystem itself, until you try to reference
|
6
|
+
it by calling methods on it. It can not, however, be relative - it is always an absolute reference
|
7
|
+
to a specific pool and path.
|
8
|
+
|
9
|
+
The only exception is when trying to reference a mountpoint by using a filesystem-path. In this
|
10
|
+
case, a ZFS object is only returned if the mountpoint exists. (eg. `ZFS('/tank/foo')`)
|
11
|
+
|
12
|
+
ZFS is mutable, and contains potentially very destructive methods.
|
13
|
+
|
14
|
+
## Usage
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
ZFS.pools # => [<ZFS:tank>]
|
18
|
+
|
19
|
+
fs = ZFS('tank/foo') # => <ZFS:tank/foo>
|
20
|
+
fs.create # creates the filesystem
|
21
|
+
fs.exist? # => true
|
22
|
+
fs.name # => 'tank/foo'
|
23
|
+
fs.mountpoint # => '/tank/foo'
|
24
|
+
|
25
|
+
ZFS('/tank/foo') # => <ZFS:tank/foo>
|
26
|
+
|
27
|
+
fs.parent # => <ZFS:tank>
|
28
|
+
fs.parent.parent # => nil
|
29
|
+
|
30
|
+
fs.available # returns bytes available in the filesystem
|
31
|
+
fs.type # => :filesystem
|
32
|
+
fs.checksum = :fletcher4
|
33
|
+
fs.readonly = true
|
34
|
+
fs.readonly? # => true
|
35
|
+
# plus all other properties defined in (currently) ZFS v28
|
36
|
+
|
37
|
+
fs['org.freebsd:swap'] = 1 # sets the custom property 'org.freebsd:swap' to 1
|
38
|
+
fs['org.freebsd:swap'] # => 1
|
39
|
+
|
40
|
+
(fs + 'bar').create # => <ZFS:tank/foo/bar>
|
41
|
+
(fs + 'bar/baz').create # => <ZFS:tank/foo/bar/baz>
|
42
|
+
fs.children # => [<ZFS:tank/foo/bar]
|
43
|
+
fs.children(recursive: true)# => [<ZFS:tank/foo/bar>, <ZFS:tank/foo/bar/baz>]
|
44
|
+
|
45
|
+
s = fs.snapshot('snapname') # => <ZFS:tank/foo@snapname>
|
46
|
+
s.parent # => <ZFS:tank/foo>
|
47
|
+
fs.snapshots # => [<ZFS:tank/foo@snapname>]
|
48
|
+
s.destroy! # destroys snapshot
|
49
|
+
|
50
|
+
# Take a recursive snapshot ('zfs snapshot -r')
|
51
|
+
fs.snapshot('snapname', children: true)
|
52
|
+
# => [<ZFS:tank/foo@snapname>, <ZFS:tank/foo/bar@snapname, ...]
|
53
|
+
|
54
|
+
# Destroy a snapshot recursively
|
55
|
+
ZFS('tank/foo@snapname').destroy!(children: true)
|
56
|
+
|
57
|
+
s = fs.snapshot('snapname') # => <ZFS:tank/foo@snapname>
|
58
|
+
fs2 = s.clone('tank/bar') # => <ZFS:tank/bar>
|
59
|
+
fs2.promote!
|
60
|
+
|
61
|
+
fs2.rename('tank/baz')
|
62
|
+
|
63
|
+
snapshot.send_to(fs) # ZFS send/receive rolled into one - needs long description
|
64
|
+
|
65
|
+
Still missing inherit, mount/unmount, share/unshare, and maybe send/receive
|
66
|
+
|
67
|
+
# Shell out to `ssh`, and assume `zfs` and `zpool` is in path on remote host
|
68
|
+
ZFS('tank/foo', hostname: 'foo.example.com')
|
69
|
+
|
70
|
+
# Can be set to either a String or an Array
|
71
|
+
ZFS.zfs_path # => '/sbin/zfs'
|
72
|
+
ZFS.zpool_path # => '/sbin/zpool'
|
73
|
+
```
|
74
|
+
|
75
|
+
## Development
|
76
|
+
|
77
|
+
Uses a Vagrant VM with a custom Ubuntu + ZFS-on-Linux to do all the practical tests, to avoid thrashing any local ZFS-installations.
|
78
|
+
|
79
|
+
|
80
|
+
## Bugs
|
81
|
+
|
82
|
+
* Currently, ZFS-objects aren't cached, so two instances can refer to the same filesystem. If mutable actions are called, only one is updated to reflect. (eg. rename!)
|
83
|
+
* Many commands take options, but do not warn/error if given invalid options
|
84
|
+
|
85
|
+
## Todo
|
86
|
+
|
87
|
+
* Support remote systems by prepending zfs/zpool-command with "ssh #{host}"
|
88
|
+
* gem
|
89
|
+
* http://docs.rubygems.org/read/chapter/20
|
90
|
+
* http://yehudakatz.com/2010/04/02/using-gemspecs-as-intended/
|
91
|
+
* Docs
|
92
|
+
* http://tomdoc.org/
|
93
|
+
* http://www.bolthole.com/solaris/zrep/src/
|
94
|
+
* Replace Open4 with Open3#popen3
|
data/Vagrantfile
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# -*- mode: ruby -*-
|
2
|
+
# vi: set ft=ruby :
|
3
|
+
|
4
|
+
Vagrant::Config.run do |config|
|
5
|
+
# All Vagrant configuration is done here. The most common configuration
|
6
|
+
# options are documented and commented below. For a complete reference,
|
7
|
+
# please see the online documentation at vagrantup.com.
|
8
|
+
|
9
|
+
# Every Vagrant virtual environment requires a box to build off of.
|
10
|
+
config.vm.box = "ubuntu-11.10+zfs-64bit"
|
11
|
+
|
12
|
+
# The url from where the 'config.vm.box' box will be fetched if it
|
13
|
+
# doesn't already exist on the user's system.
|
14
|
+
# config.vm.box_url = "http://domain.com/path/to/above.box"
|
15
|
+
|
16
|
+
# Boot with a GUI so you can see the screen. (Default is headless)
|
17
|
+
# config.vm.boot_mode = :gui
|
18
|
+
|
19
|
+
# Assign this VM to a host-only network IP, allowing you to access it
|
20
|
+
# via the IP. Host-only networks can talk to the host machine as well as
|
21
|
+
# any other machines on the same network, but cannot be accessed (through this
|
22
|
+
# network interface) by any external networks.
|
23
|
+
# config.vm.network :hostonly, "33.33.33.10"
|
24
|
+
|
25
|
+
# Assign this VM to a bridged network, allowing you to connect directly to a
|
26
|
+
# network using the host's network device. This makes the VM appear as another
|
27
|
+
# physical device on your network.
|
28
|
+
# config.vm.network :bridged
|
29
|
+
|
30
|
+
# Forward a port from the guest to the host, which allows for outside
|
31
|
+
# computers to access the VM, whereas host only networking does not.
|
32
|
+
# config.vm.forward_port 80, 8080
|
33
|
+
|
34
|
+
# Share an additional folder to the guest VM. The first argument is
|
35
|
+
# an identifier, the second is the path on the guest to mount the
|
36
|
+
# folder, and the third is the path on the host to the actual folder.
|
37
|
+
# config.vm.share_folder "v-data", "/vagrant_data", "../data"
|
38
|
+
end
|
data/lib/zfs.rb
ADDED
@@ -0,0 +1,513 @@
|
|
1
|
+
# -*- mode: ruby; tab-width: 4; indent-tabs-mode: t -*-
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
require 'date'
|
5
|
+
require 'open4'
|
6
|
+
|
7
|
+
# Get ZFS object.
|
8
|
+
def ZFS(path)
|
9
|
+
return path if path.is_a? ZFS
|
10
|
+
|
11
|
+
path = Pathname(path).cleanpath.to_s
|
12
|
+
|
13
|
+
if path.match(/^\//)
|
14
|
+
ZFS.mounts[path]
|
15
|
+
elsif path.match('@')
|
16
|
+
ZFS::Snapshot.new(path)
|
17
|
+
else
|
18
|
+
ZFS::Filesystem.new(path)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Pathname-inspired class to handle ZFS filesystems/snapshots/volumes
|
23
|
+
class ZFS
|
24
|
+
@zfs_path = "zfs"
|
25
|
+
@zpool_path = "zpool"
|
26
|
+
|
27
|
+
attr_reader :name
|
28
|
+
attr_reader :pool
|
29
|
+
attr_reader :path
|
30
|
+
|
31
|
+
class NotFound < Exception; end
|
32
|
+
class AlreadyExists < Exception; end
|
33
|
+
class InvalidName < Exception; end
|
34
|
+
|
35
|
+
# Create a new ZFS object (_not_ filesystem).
|
36
|
+
def initialize(name)
|
37
|
+
@name, @pool, @path = name, *name.split('/', 2)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Return the parent of the current filesystem, or nil if there is none.
|
41
|
+
def parent
|
42
|
+
p = Pathname(name).parent.to_s
|
43
|
+
if p == '.'
|
44
|
+
nil
|
45
|
+
else
|
46
|
+
ZFS(p)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns the children of this filesystem
|
51
|
+
def children(opts={})
|
52
|
+
raise NotFound if !exist?
|
53
|
+
|
54
|
+
stdout, stderr = [], []
|
55
|
+
cmd = [ZFS.zfs_path].flatten + %w(list -H -r -oname -tfilesystem)
|
56
|
+
cmd << '-d1' unless opts[:recursive]
|
57
|
+
cmd << name
|
58
|
+
|
59
|
+
Open4::spawn(cmd, stdout: stdout, stderr: stderr)
|
60
|
+
|
61
|
+
stdout.shift # self
|
62
|
+
stdout.collect do |filesystem|
|
63
|
+
ZFS(filesystem.chomp)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Does the filesystem exist?
|
68
|
+
def exist?
|
69
|
+
stdout, stderr = [], []
|
70
|
+
cmd = [ZFS.zfs_path].flatten + %w(list -H -oname) + [name]
|
71
|
+
|
72
|
+
Open4::spawn(cmd, stdout: stdout, stderr: stderr, ignore_exit_failure: true)
|
73
|
+
|
74
|
+
if stdout == ["#{name}\n"]
|
75
|
+
true
|
76
|
+
else
|
77
|
+
false
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Create filesystem
|
82
|
+
def create(opts={})
|
83
|
+
return nil if exist?
|
84
|
+
|
85
|
+
stdout, stderr = [], []
|
86
|
+
cmd = [ZFS.zfs_path].flatten + ['create']
|
87
|
+
cmd << '-p' if opts[:parents]
|
88
|
+
cmd += ['-V', opts[:volume]] if opts[:volume]
|
89
|
+
cmd << name
|
90
|
+
|
91
|
+
Open4::spawn(cmd, stdout: stdout, stderr: stderr)
|
92
|
+
|
93
|
+
if stdout.empty? and stderr.empty?
|
94
|
+
return self
|
95
|
+
else
|
96
|
+
raise Exception, "something went wrong"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Destroy filesystem
|
101
|
+
def destroy!(opts={})
|
102
|
+
raise NotFound if !exist?
|
103
|
+
|
104
|
+
stdout, stderr = [], []
|
105
|
+
cmd = [ZFS.zfs_path].flatten + ['destroy']
|
106
|
+
cmd << '-r' if opts[:children]
|
107
|
+
cmd << name
|
108
|
+
|
109
|
+
Open4::spawn(cmd, stdout: stdout, stderr: stderr)
|
110
|
+
|
111
|
+
if stdout.empty? and stderr.empty?
|
112
|
+
return true
|
113
|
+
else
|
114
|
+
raise Exception, "something went wrong"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Stringify
|
119
|
+
def to_s
|
120
|
+
"#<ZFS:#{name}>"
|
121
|
+
end
|
122
|
+
|
123
|
+
# ZFS's are considered equal if they are the same class and name
|
124
|
+
def ==(other)
|
125
|
+
other.class == self.class && other.name == self.name
|
126
|
+
end
|
127
|
+
|
128
|
+
def [](key)
|
129
|
+
stdout, stderr = [], []
|
130
|
+
cmd = [ZFS.zfs_path].flatten + %w(get -ovalue -Hp) + [key.to_s, name]
|
131
|
+
|
132
|
+
Open4::spawn(cmd, stdout: stdout, stderr: stderr)
|
133
|
+
|
134
|
+
if stderr.empty? and stdout.size == 1
|
135
|
+
return stdout.first.chomp
|
136
|
+
else
|
137
|
+
raise Exception, "something went wrong"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def []=(key, value)
|
142
|
+
stdout, stderr = [], []
|
143
|
+
cmd = [ZFS.zfs_path].flatten + ['set', "#{key.to_s}=#{value}", name]
|
144
|
+
|
145
|
+
Open4::spawn(cmd, stdout: stdout, stderr: stderr)
|
146
|
+
|
147
|
+
if stderr.empty? and stdout.empty?
|
148
|
+
return value
|
149
|
+
else
|
150
|
+
raise Exception, "something went wrong"
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
class << self
|
155
|
+
attr_accessor :zfs_path
|
156
|
+
attr_accessor :zpool_path
|
157
|
+
|
158
|
+
# Get an Array of all pools
|
159
|
+
def pools
|
160
|
+
stdout, stderr = [], []
|
161
|
+
cmd = [ZFS.zpool_path].flatten + %w(list -Honame)
|
162
|
+
|
163
|
+
Open4::spawn(cmd, stdout: stdout, stderr: stderr)
|
164
|
+
|
165
|
+
stdout.collect do |pool|
|
166
|
+
ZFS(pool.chomp)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Get a Hash of all mountpoints and their filesystems
|
171
|
+
def mounts
|
172
|
+
stdout, stderr = [], []
|
173
|
+
cmd = [ZFS.zfs_path].flatten + %w(get -rHp -oname,value mountpoint)
|
174
|
+
|
175
|
+
Open4::spawn(cmd, stdout: stdout, stderr: stderr)
|
176
|
+
|
177
|
+
mounts = stdout.collect do |line|
|
178
|
+
fs, path = line.chomp.split(/\t/, 2)
|
179
|
+
[path, ZFS(fs)]
|
180
|
+
end
|
181
|
+
Hash[mounts]
|
182
|
+
end
|
183
|
+
|
184
|
+
# Define an attribute
|
185
|
+
def property(name, opts={})
|
186
|
+
|
187
|
+
case opts[:type]
|
188
|
+
when :size, :integer
|
189
|
+
# FIXME: also takes :values. if :values is all-Integers, these are the only options. if there are non-ints, then :values is a supplement
|
190
|
+
|
191
|
+
define_method name do
|
192
|
+
Integer(self[name])
|
193
|
+
end
|
194
|
+
define_method "#{name}=" do |value|
|
195
|
+
self[name] = value.to_s
|
196
|
+
end if opts[:edit]
|
197
|
+
|
198
|
+
when :boolean
|
199
|
+
# FIXME: booleans can take extra values, so there are on/true, off/false, plus what amounts to an enum
|
200
|
+
# FIXME: if options[:values] is defined, also create a 'name' method, since 'name?' might not ring true
|
201
|
+
# FIXME: replace '_' by '-' in opts[:values]
|
202
|
+
define_method "#{name}?" do
|
203
|
+
self[name] == 'on'
|
204
|
+
end
|
205
|
+
define_method "#{name}=" do |value|
|
206
|
+
self[name] = value ? 'on' : 'off'
|
207
|
+
end if opts[:edit]
|
208
|
+
|
209
|
+
when :enum
|
210
|
+
define_method name do
|
211
|
+
sym = (self[name] || "").gsub('-', '_').to_sym
|
212
|
+
if opts[:values].grep(sym)
|
213
|
+
return sym
|
214
|
+
else
|
215
|
+
raise "#{name} has value #{sym}, which is not in enum-list"
|
216
|
+
end
|
217
|
+
end
|
218
|
+
define_method "#{name}=" do |value|
|
219
|
+
self[name] = value.to_s.gsub('_', '-')
|
220
|
+
end if opts[:edit]
|
221
|
+
|
222
|
+
when :snapshot
|
223
|
+
define_method name do
|
224
|
+
val = self[name]
|
225
|
+
if val.nil? or val == '-'
|
226
|
+
nil
|
227
|
+
else
|
228
|
+
ZFS(val)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
when :float
|
233
|
+
define_method name do
|
234
|
+
Float(self[name])
|
235
|
+
end
|
236
|
+
define_method "#{name}=" do |value|
|
237
|
+
self[name] = value
|
238
|
+
end if opts[:edit]
|
239
|
+
|
240
|
+
when :string
|
241
|
+
define_method name do
|
242
|
+
self[name]
|
243
|
+
end
|
244
|
+
define_method "#{name}=" do |value|
|
245
|
+
self[name] = value
|
246
|
+
end if opts[:edit]
|
247
|
+
|
248
|
+
when :date
|
249
|
+
define_method name do
|
250
|
+
DateTime.strptime(self[name], '%s')
|
251
|
+
end
|
252
|
+
|
253
|
+
when :pathname
|
254
|
+
define_method name do
|
255
|
+
Pathname(self[name])
|
256
|
+
end
|
257
|
+
define_method "#{name}=" do |value|
|
258
|
+
self[name] = value.to_s
|
259
|
+
end if opts[:edit]
|
260
|
+
|
261
|
+
else
|
262
|
+
puts "Unknown type '#{opts[:type]}'"
|
263
|
+
end
|
264
|
+
end
|
265
|
+
private :property
|
266
|
+
end
|
267
|
+
|
268
|
+
property :available, type: :size
|
269
|
+
property :compressratio, type: :float
|
270
|
+
property :creation, type: :date
|
271
|
+
property :defer_destroy, type: :boolean
|
272
|
+
property :mounted, type: :boolean
|
273
|
+
property :origin, type: :snapshot
|
274
|
+
property :refcompressratio, type: :float
|
275
|
+
property :referenced, type: :size
|
276
|
+
property :type, type: :enum, values: [:filesystem, :snapshot, :volume]
|
277
|
+
property :used, type: :size
|
278
|
+
property :usedbychildren, type: :size
|
279
|
+
property :usedbydataset, type: :size
|
280
|
+
property :usedbyrefreservation, type: :size
|
281
|
+
property :usedbysnapshots, type: :size
|
282
|
+
property :userrefs, type: :integer
|
283
|
+
|
284
|
+
property :aclinherit, type: :enum, edit: true, inherit: true, values: [:discard, :noallow, :restricted, :passthrough, :passthrough_x]
|
285
|
+
property :atime, type: :boolean, edit: true, inherit: true
|
286
|
+
property :canmount, type: :boolean, edit: true, values: [:noauto]
|
287
|
+
property :checksum, type: :boolean, edit: true, inherit: true, values: [:fletcher2, :fletcher4, :sha256]
|
288
|
+
property :compression, type: :boolean, edit: true, inherit: true, values: [:lzjb, :gzip, :gzip_1, :gzip_2, :gzip_3, :gzip_4, :gzip_5, :gzip_6, :gzip_7, :gzip_8, :gzip_9, :zle]
|
289
|
+
property :copies, type: :integer, edit: true, inherit: true, values: [1, 2, 3]
|
290
|
+
property :dedup, type: :boolean, edit: true, inherit: true, values: [:verify, :sha256, 'sha256,verify']
|
291
|
+
property :devices, type: :boolean, edit: true, inherit: true
|
292
|
+
property :exec, type: :boolean, edit: true, inherit: true
|
293
|
+
property :logbias, type: :enum, edit: true, inherit: true, values: [:latency, :throughput]
|
294
|
+
property :mlslabel, type: :string, edit: true, inherit: true
|
295
|
+
property :mountpoint, type: :pathname,edit: true, inherit: true
|
296
|
+
property :nbmand, type: :boolean, edit: true, inherit: true
|
297
|
+
property :primarycache, type: :enum, edit: true, inherit: true, values: [:all, :none, :metadata]
|
298
|
+
property :quota, type: :size, edit: true, values: [:none]
|
299
|
+
property :readonly, type: :boolean, edit: true, inherit: true
|
300
|
+
property :recordsize, type: :integer, edit: true, inherit: true, values: [512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072]
|
301
|
+
property :refquota, type: :size, edit: true, values: [:none]
|
302
|
+
property :refreservation, type: :size, edit: true, values: [:none]
|
303
|
+
property :reservation, type: :size, edit: true, values: [:none]
|
304
|
+
property :secondarycache, type: :enum, edit: true, inherit: true, values: [:all, :none, :metadata]
|
305
|
+
property :setuid, type: :boolean, edit: true, inherit: true
|
306
|
+
property :sharenfs, type: :boolean, edit: true, inherit: true # FIXME: also takes 'share(1M) options'
|
307
|
+
property :sharesmb, type: :boolean, edit: true, inherit: true # FIXME: also takes 'sharemgr(1M) options'
|
308
|
+
property :snapdir, type: :enum, edit: true, inherit: true, values: [:hidden, :visible]
|
309
|
+
property :sync, type: :enum, edit: true, inherit: true, values: [:standard, :always, :disabled]
|
310
|
+
property :version, type: :integer, edit: true, values: [1, 2, 3, 4, :current]
|
311
|
+
property :vscan, type: :boolean, edit: true, inherit: true
|
312
|
+
property :xattr, type: :boolean, edit: true, inherit: true
|
313
|
+
property :zoned, type: :boolean, edit: true, inherit: true
|
314
|
+
property :jailed, type: :boolean, edit: true, inherit: true
|
315
|
+
property :volsize, type: :size, edit: true
|
316
|
+
|
317
|
+
property :casesensitivity, type: :enum, create_only: true, values: [:sensitive, :insensitive, :mixed]
|
318
|
+
property :normalization, type: :enum, create_only: true, values: [:none, :formC, :formD, :formKC, :formKD]
|
319
|
+
property :utf8only, type: :boolean, create_only: true
|
320
|
+
property :volblocksize, type: :integer, create_only: true, values: [512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072]
|
321
|
+
end
|
322
|
+
|
323
|
+
|
324
|
+
class ZFS::Snapshot < ZFS
|
325
|
+
# Return sub-filesystem
|
326
|
+
def +(path)
|
327
|
+
raise InvalidName if path.match(/@/)
|
328
|
+
|
329
|
+
parent + path + name.sub(/^.+@/, '@')
|
330
|
+
end
|
331
|
+
|
332
|
+
# Just remove the snapshot-name
|
333
|
+
def parent
|
334
|
+
ZFS(name.sub(/@.+/, ''))
|
335
|
+
end
|
336
|
+
|
337
|
+
# Rename snapshot
|
338
|
+
def rename!(newname, opts={})
|
339
|
+
raise AlreadyExists if (parent + "@#{newname}").exist?
|
340
|
+
|
341
|
+
newname = (parent + "@#{newname}").name
|
342
|
+
|
343
|
+
stdout, stderr = [], []
|
344
|
+
cmd = [ZFS.zfs_path].flatten + ['rename']
|
345
|
+
cmd << '-r' if opts[:children]
|
346
|
+
cmd << name
|
347
|
+
cmd << newname
|
348
|
+
|
349
|
+
Open4::spawn(cmd, stdout: stdout, stderr: stderr)
|
350
|
+
|
351
|
+
if stdout.empty? and stderr.empty?
|
352
|
+
initialize(newname)
|
353
|
+
return self
|
354
|
+
else
|
355
|
+
raise Exception, "something went wrong"
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
# Clone snapshot
|
360
|
+
def clone!(clone, opts={})
|
361
|
+
clone = clone.name if clone.is_a? ZFS
|
362
|
+
|
363
|
+
raise AlreadyExists if ZFS(clone).exist?
|
364
|
+
|
365
|
+
stdout, stderr = [], []
|
366
|
+
cmd = [ZFS.zfs_path].flatten + ['clone']
|
367
|
+
cmd << '-p' if opts[:parents]
|
368
|
+
cmd << name
|
369
|
+
cmd << clone
|
370
|
+
|
371
|
+
Open4::spawn(cmd, stdout: stdout, stderr: stderr)
|
372
|
+
|
373
|
+
if stdout.empty? and stderr.empty?
|
374
|
+
return ZFS(clone)
|
375
|
+
else
|
376
|
+
raise Exception, "something went wrong"
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
# Send snapshot to another filesystem
|
381
|
+
def send_to(dest, opts={})
|
382
|
+
incr_snap = nil
|
383
|
+
dest = ZFS(dest)
|
384
|
+
|
385
|
+
if opts[:incremental] and opts[:intermediary]
|
386
|
+
raise ArgumentError, "can't specify both :incremental and :intermediary"
|
387
|
+
end
|
388
|
+
|
389
|
+
incr_snap = opts[:incremental] || opts[:intermediary]
|
390
|
+
if incr_snap
|
391
|
+
if incr_snap.is_a? String and incr_snap.match(/^@/)
|
392
|
+
incr_snap = self.parent + incr_snap
|
393
|
+
else
|
394
|
+
incr_snap = ZFS(incr_snap)
|
395
|
+
raise ArgumentError, "incremental snapshot must be in the same filesystem as #{self}" if incr_snap.parent != self.parent
|
396
|
+
end
|
397
|
+
|
398
|
+
snapname = incr_snap.name.sub(/^.+@/, '@')
|
399
|
+
|
400
|
+
raise NotFound, "destination must already exist when receiving incremental stream" unless dest.exist?
|
401
|
+
raise NotFound, "snapshot #{snapname} must exist at #{self.parent}" if self.parent.snapshots.grep(incr_snap).empty?
|
402
|
+
raise NotFound, "snapshot #{snapname} must exist at #{dest}" if dest.snapshots.grep(dest + snapname).empty?
|
403
|
+
elsif opts[:use_sent_name]
|
404
|
+
raise NotFound, "destination must already exist when using sent name" unless dest.exist?
|
405
|
+
elsif dest.exist?
|
406
|
+
raise AlreadyExists, "destination must not exist when receiving full stream"
|
407
|
+
end
|
408
|
+
|
409
|
+
dest = dest.name if dest.is_a? ZFS
|
410
|
+
incr_snap = incr_snap.name if incr_snap.is_a? ZFS
|
411
|
+
|
412
|
+
send_opts = ZFS.zfs_path.flatten + ['send']
|
413
|
+
send_opts.concat ['-i', incr_snap] if opts[:incremental]
|
414
|
+
send_opts.concat ['-I', incr_snap] if opts[:intermediary]
|
415
|
+
send_opts << '-R' if opts[:replication]
|
416
|
+
send_opts << name
|
417
|
+
|
418
|
+
receive_opts = ZFS.zfs_path.flatten + ['receive']
|
419
|
+
receive_opts << '-d' if opts[:use_sent_name]
|
420
|
+
receive_opts << dest
|
421
|
+
|
422
|
+
Open4::popen4(*receive_opts) do |rpid, rstdin, rstdout, rstderr|
|
423
|
+
Open4::popen4(*send_opts) do |spid, sstdin, sstdout, sstderr|
|
424
|
+
while !sstdout.eof?
|
425
|
+
rstdin.write(sstdout.read(16384))
|
426
|
+
end
|
427
|
+
raise "stink" unless sstderr.read == ''
|
428
|
+
end
|
429
|
+
end
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
|
434
|
+
class ZFS::Filesystem < ZFS
|
435
|
+
# Return sub-filesystem.
|
436
|
+
def +(path)
|
437
|
+
if path.match(/^@/)
|
438
|
+
ZFS("#{name.to_s}#{path}")
|
439
|
+
else
|
440
|
+
path = Pathname(name) + path
|
441
|
+
ZFS(path.cleanpath.to_s)
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
# Rename filesystem.
|
446
|
+
def rename!(newname, opts={})
|
447
|
+
raise AlreadyExists if ZFS(newname).exist?
|
448
|
+
|
449
|
+
stdout, stderr = [], []
|
450
|
+
cmd = [ZFS.zfs_path].flatten + ['rename']
|
451
|
+
cmd << '-p' if opts[:parents]
|
452
|
+
cmd << name
|
453
|
+
cmd << newname
|
454
|
+
|
455
|
+
Open4::spawn(cmd, stdout: stdout, stderr: stderr)
|
456
|
+
|
457
|
+
if stdout.empty? and stderr.empty?
|
458
|
+
initialize(newname)
|
459
|
+
return self
|
460
|
+
else
|
461
|
+
raise Exception, "something went wrong"
|
462
|
+
end
|
463
|
+
end
|
464
|
+
|
465
|
+
# Create a snapshot.
|
466
|
+
def snapshot(snapname, opts={})
|
467
|
+
raise NotFound, "no such filesystem" if !exist?
|
468
|
+
raise AlreadyExists, "#{snapname} exists" if ZFS("#{name}@#{snapname}").exist?
|
469
|
+
|
470
|
+
stdout, stderr = [], []
|
471
|
+
cmd = [ZFS.zfs_path].flatten + ['snapshot']
|
472
|
+
cmd << '-r' if opts[:children]
|
473
|
+
cmd << "#{name}@#{snapname}"
|
474
|
+
|
475
|
+
Open4::spawn(cmd, stdout: stdout, stderr: stderr)
|
476
|
+
|
477
|
+
if stdout.empty? and stderr.empty?
|
478
|
+
return ZFS("#{name}@#{snapname}")
|
479
|
+
else
|
480
|
+
raise Exception, "something went wrong"
|
481
|
+
end
|
482
|
+
end
|
483
|
+
|
484
|
+
# Get an Array of all snapshots on this filesystem.
|
485
|
+
def snapshots
|
486
|
+
raise NotFound, "no such filesystem" if !exist?
|
487
|
+
|
488
|
+
stdout, stderr = [], []
|
489
|
+
cmd = [ZFS.zfs_path].flatten + %w(list -H -d1 -r -oname -tsnapshot) + [name]
|
490
|
+
|
491
|
+
Open4::spawn(cmd, stdout: stdout, stderr: stderr)
|
492
|
+
|
493
|
+
stdout.collect do |snap|
|
494
|
+
ZFS(snap.chomp)
|
495
|
+
end
|
496
|
+
end
|
497
|
+
|
498
|
+
# Promote this filesystem.
|
499
|
+
def promote!
|
500
|
+
raise NotFound, "filesystem is not a clone" if self.origin.nil?
|
501
|
+
|
502
|
+
stdout, stderr = [], []
|
503
|
+
cmd = [ZFS.zfs_path].flatten + ['promote', name]
|
504
|
+
|
505
|
+
Open4::spawn(cmd, stdout: stdout, stderr: stderr)
|
506
|
+
|
507
|
+
if stdout.empty? and stderr.empty?
|
508
|
+
return self
|
509
|
+
else
|
510
|
+
raise Exception, "something went wrong"
|
511
|
+
end
|
512
|
+
end
|
513
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- mode: ruby; tab-width: 4; indent-tabs-mode: t -*-
|
2
|
+
$LOAD_PATH.push(File.expand_path('../lib/zfs'))
|
3
|
+
|
4
|
+
require 'zfs'
|
5
|
+
require 'open4'
|
6
|
+
|
7
|
+
shared_context "vagrant" do
|
8
|
+
before(:all) do
|
9
|
+
ZFS.zfs_path = %w(ssh vagrant-zfs sudo zfs)
|
10
|
+
ZFS.zpool_path = %w(ssh vagrant-zfs sudo zpool)
|
11
|
+
end
|
12
|
+
|
13
|
+
after(:all) do
|
14
|
+
Open4::spawn([*ZFS.zfs_path]+['destroy -r tank/foo'], ignore_exit_failure: true)
|
15
|
+
Open4::spawn([*ZFS.zfs_path]+['destroy -r tank/bar'], ignore_exit_failure: true)
|
16
|
+
|
17
|
+
ZFS.zfs_path = 'zfs'
|
18
|
+
ZFS.zpool_path = 'zpool'
|
19
|
+
end
|
20
|
+
end
|
data/spec/zfs_spec.rb
ADDED
@@ -0,0 +1,518 @@
|
|
1
|
+
# -*- mode: ruby; tab-width: 4; indent-tabs-mode: t -*-
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
# NOTE! The ordering of the tests may be important, or at least significant.
|
5
|
+
# Example: we test create+snapshot, then destroy - in between, there is state left behind
|
6
|
+
# since we don't want to assume something works before it's been tested, to avoid very
|
7
|
+
# confusing errors in stuff other than what is being tested.
|
8
|
+
#
|
9
|
+
# We also test clones after snapshots, and promote! after clones and properties.
|
10
|
+
|
11
|
+
|
12
|
+
# Helper-method to return ZFS-instances
|
13
|
+
describe "ZFS()" do
|
14
|
+
it "returns the correct instance" do
|
15
|
+
ZFS('tank').should be_an_instance_of ZFS::Filesystem
|
16
|
+
ZFS('tank@foo').should be_an_instance_of ZFS::Snapshot
|
17
|
+
end
|
18
|
+
|
19
|
+
it "supports Pathname's" do
|
20
|
+
ZFS('tank').should eq ZFS(Pathname('tank'))
|
21
|
+
end
|
22
|
+
|
23
|
+
it "passes a ZFS as argument through untouched" do
|
24
|
+
fs = ZFS('tank')
|
25
|
+
ZFS(fs).should eq fs
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
# Methods that don't require a live filesystem
|
31
|
+
describe ZFS do
|
32
|
+
describe "#parent" do
|
33
|
+
it "returns the correct parent" do
|
34
|
+
ZFS('tank/foo/bar').parent.should eq ZFS('tank/foo')
|
35
|
+
ZFS('tank/foo/bar@snap').parent.should eq ZFS('tank/foo/bar')
|
36
|
+
ZFS('tank/foo/bar@snap').parent.parent.should eq ZFS('tank/foo')
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "#+" do
|
41
|
+
it "returns an appended path" do
|
42
|
+
(ZFS('tank/foo') + 'bar').should eq ZFS('tank/foo/bar')
|
43
|
+
(ZFS('tank/foo') + '@bar').should eq ZFS('tank/foo@bar')
|
44
|
+
(ZFS('tank/foo@baz') + 'bar').should eq ZFS('tank/foo/bar@baz')
|
45
|
+
expect { (ZFS('tank/foo@baz') + '@bar') }.to raise_exception ZFS::InvalidName
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
# Methods that require a live filesystem.
|
52
|
+
describe ZFS do
|
53
|
+
include_context "vagrant"
|
54
|
+
|
55
|
+
describe "#create" do
|
56
|
+
it "creates a filesystem, and returns the filesystem or nil if it already exists" do
|
57
|
+
ZFS('tank/foo').should_not exist
|
58
|
+
ZFS('tank/foo').create.should eq ZFS('tank/foo')
|
59
|
+
ZFS('tank/foo').create.should be_nil
|
60
|
+
ZFS('tank/foo').should exist
|
61
|
+
end
|
62
|
+
|
63
|
+
it "creates parents" do
|
64
|
+
ZFS('tank/foo/bar/baz').create(parents: true).should exist
|
65
|
+
end
|
66
|
+
|
67
|
+
it "raises an error if parent does not exist"
|
68
|
+
|
69
|
+
it "creates volumes" do
|
70
|
+
ZFS('tank/foo/volume').create(volume: '10G').should_not be_nil
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe "#snapshot" do
|
75
|
+
it "creates a snapshot" do
|
76
|
+
snapshot = ZFS('tank/foo').snapshot('qux')
|
77
|
+
|
78
|
+
snapshot.should be_an_instance_of ZFS::Snapshot
|
79
|
+
end
|
80
|
+
|
81
|
+
it "creates a snapshot recursively" do
|
82
|
+
ZFS('tank/foo').snapshot('quux', children: true)
|
83
|
+
ZFS('tank/foo@quux').should exist
|
84
|
+
ZFS('tank/foo/bar@quux').should exist
|
85
|
+
ZFS('tank/foo/bar/baz@quux').should exist
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should raise an exception if filesystem does not exist" do
|
89
|
+
expect { ZFS('tank/none').snapshot('foo') }.to raise_error(ZFS::NotFound)
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should raise an exception if snapshot already exists" do
|
93
|
+
expect { ZFS('tank/foo').snapshot('quux') }.to raise_error(ZFS::AlreadyExists)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe "#destroy" do
|
98
|
+
it "destroys snapshots" do
|
99
|
+
ZFS('tank/foo/bar/baz@quux').should exist
|
100
|
+
ZFS('tank/foo/bar/baz@quux').destroy!
|
101
|
+
ZFS('tank/foo/bar/baz@quux').should_not exist
|
102
|
+
end
|
103
|
+
|
104
|
+
it "destroys filesystems" do
|
105
|
+
ZFS('tank/foo/bar/baz').should exist
|
106
|
+
ZFS('tank/foo/bar/baz').destroy!
|
107
|
+
ZFS('tank/foo/bar/baz').should_not exist
|
108
|
+
end
|
109
|
+
|
110
|
+
it "destroys snapshots recursively" do
|
111
|
+
ZFS('tank/foo@quux').should exist
|
112
|
+
ZFS('tank/foo/bar@quux').should exist
|
113
|
+
|
114
|
+
ZFS('tank/foo@quux').destroy!(children: true)
|
115
|
+
|
116
|
+
ZFS('tank/foo@quux').should_not exist
|
117
|
+
ZFS('tank/foo/bar@quux').should_not exist
|
118
|
+
|
119
|
+
ZFS('tank/foo').should exist
|
120
|
+
ZFS('tank/foo/bar').should exist
|
121
|
+
end
|
122
|
+
|
123
|
+
it "destroys filesystems recursively" do
|
124
|
+
ZFS('tank/foo').destroy!(children: true)
|
125
|
+
ZFS('tank/foo').should_not exist
|
126
|
+
end
|
127
|
+
|
128
|
+
it "should raise an exception if filesystem does not exist" do
|
129
|
+
expect { ZFS('tank/none').destroy! }.to raise_error(ZFS::NotFound)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
describe "#snapshots" do
|
134
|
+
it "returns a list of snapshots on a filesystem" do
|
135
|
+
snapshot = ZFS('tank/foo').create.snapshot('bar')
|
136
|
+
snapshot.should exist
|
137
|
+
|
138
|
+
ZFS('tank/foo').snapshots.should eq [snapshot]
|
139
|
+
|
140
|
+
snapshot2 = ZFS('tank/foo').snapshot('baz')
|
141
|
+
|
142
|
+
ZFS('tank/foo').snapshots.should eq [snapshot, snapshot2]
|
143
|
+
|
144
|
+
# Should only include direct descendants
|
145
|
+
ZFS('tank/foo/bar').create.snapshot('baz')
|
146
|
+
ZFS('tank/foo').snapshots.should eq [snapshot, snapshot2]
|
147
|
+
|
148
|
+
snapshot.destroy!
|
149
|
+
ZFS('tank/foo').snapshots.should eq [snapshot2]
|
150
|
+
|
151
|
+
ZFS('tank/foo').destroy!(children: true)
|
152
|
+
|
153
|
+
ZFS('tank/foo').should_not exist
|
154
|
+
end
|
155
|
+
|
156
|
+
it "should raise an exception if filesystem does not exist" do
|
157
|
+
expect { ZFS('tank/none').snapshots }.to raise_error(ZFS::NotFound)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
describe "#children" do
|
162
|
+
before(:all) do
|
163
|
+
%w(tank/l1/ll1/lll1 tank/l1/ll2 tank/l2/ll2/lll2 tank/l3).each { |f| ZFS(f).create(parents: true) }
|
164
|
+
end
|
165
|
+
|
166
|
+
after(:all) do
|
167
|
+
%w(tank/l1 tank/l2 tank/l3).each { |f| ZFS(f).destroy!(children: true) }
|
168
|
+
end
|
169
|
+
|
170
|
+
it "should raise an exception if filesystem does not exist" do
|
171
|
+
expect { ZFS('tank/none').children }.to raise_error(ZFS::NotFound)
|
172
|
+
end
|
173
|
+
|
174
|
+
it "returns a list of immediate children" do
|
175
|
+
ZFS('tank/l1').children.should eq [ZFS('tank/l1/ll1'), ZFS('tank/l1/ll2')]
|
176
|
+
ZFS('tank').children.should eq [ZFS('tank/l1'), ZFS('tank/l2'), ZFS('tank/l3')]
|
177
|
+
end
|
178
|
+
|
179
|
+
it "returns a list of all children" do
|
180
|
+
ZFS('tank/l1').children(recursive: true).should eq [ZFS('tank/l1/ll1'), ZFS('tank/l1/ll1/lll1'), ZFS('tank/l1/ll2')]
|
181
|
+
end
|
182
|
+
|
183
|
+
it "does not include snapshots" do
|
184
|
+
ZFS('tank/l1').snapshot('test')
|
185
|
+
ZFS('tank/l1').children.should eq [ZFS('tank/l1/ll1'), ZFS('tank/l1/ll2')]
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
describe "#rename" do
|
190
|
+
it "renames filesystems" do
|
191
|
+
fs = ZFS('tank/foo').create
|
192
|
+
fs.rename!('tank/bar')
|
193
|
+
|
194
|
+
ZFS('tank/foo').should_not exist
|
195
|
+
ZFS('tank/bar').should exist
|
196
|
+
fs.should exist
|
197
|
+
|
198
|
+
fs.name.should eq "tank/bar"
|
199
|
+
|
200
|
+
fs.destroy!
|
201
|
+
end
|
202
|
+
|
203
|
+
it "renames filesystems and creates parents for new name" do
|
204
|
+
fs = ZFS('tank/foo').create
|
205
|
+
fs.rename!('tank/bar/baz/qux', parents: true)
|
206
|
+
|
207
|
+
ZFS('tank/foo').should_not exist
|
208
|
+
ZFS('tank/bar/baz/qux').should exist
|
209
|
+
fs.should exist
|
210
|
+
|
211
|
+
ZFS('tank/bar').destroy!(children: true)
|
212
|
+
end
|
213
|
+
|
214
|
+
it "renames snapshots" do
|
215
|
+
snapshot = ZFS('tank/foo').create.snapshot('foo')
|
216
|
+
snapshot.rename!('baz')
|
217
|
+
ZFS('tank/foo').snapshots.should eq [ZFS('tank/foo@baz')]
|
218
|
+
snapshot.name.should eq "tank/foo@baz"
|
219
|
+
|
220
|
+
ZFS('tank/foo').destroy!(children: true)
|
221
|
+
end
|
222
|
+
|
223
|
+
it "renames snapshots recursively" do
|
224
|
+
ZFS('tank/foo/bar/baz').create(parents: true)
|
225
|
+
ZFS('tank/foo').snapshot('bar', children: true)
|
226
|
+
|
227
|
+
ZFS('tank/foo/bar/baz@bar').should exist
|
228
|
+
|
229
|
+
ZFS('tank/foo@bar').rename!('foo', children: true)
|
230
|
+
|
231
|
+
ZFS('tank/foo/bar/baz@foo').should exist
|
232
|
+
|
233
|
+
ZFS('tank/foo').destroy!(children: true)
|
234
|
+
end
|
235
|
+
|
236
|
+
it "throws an exception when new name is already used" do
|
237
|
+
ZFS('tank/foo').create
|
238
|
+
|
239
|
+
# Rename filesystem
|
240
|
+
expect { ZFS('tank/bar').create.rename!('tank/foo') }.to raise_error ZFS::AlreadyExists
|
241
|
+
|
242
|
+
# Rename snapshot
|
243
|
+
snapshot = ZFS('tank/foo').snapshot('foo')
|
244
|
+
ZFS('tank/foo').snapshot('bar')
|
245
|
+
expect { snapshot.rename!('bar') }.to raise_error ZFS::AlreadyExists
|
246
|
+
|
247
|
+
ZFS('tank/foo').destroy!(children: true)
|
248
|
+
ZFS('tank/bar').destroy!
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
describe ".pools" do
|
253
|
+
it "returns an Array of all pools" do
|
254
|
+
ZFS.pools.should eq [ZFS('tank')]
|
255
|
+
|
256
|
+
# and only pools
|
257
|
+
ZFS('tank/foo').create
|
258
|
+
ZFS.pools.should eq [ZFS('tank')]
|
259
|
+
ZFS('tank/foo').destroy!
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
describe ".mounts" do
|
264
|
+
it "returns a Hash of all mountpoints" do
|
265
|
+
ZFS.mounts.should eq "/tank" => ZFS('tank')
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
describe "#[]" do
|
270
|
+
it "gets raw properties" do
|
271
|
+
ZFS('tank')['type'].should eq 'filesystem'
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
describe "#[]=" do
|
276
|
+
it "sets raw properties" do
|
277
|
+
ZFS('tank/foo').create
|
278
|
+
ZFS('tank/foo')['exec'].should eq 'on'
|
279
|
+
ZFS('tank/foo')['exec'] = 'off'
|
280
|
+
ZFS('tank/foo')['exec'].should eq 'off'
|
281
|
+
ZFS('tank/foo').destroy!
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
describe "properties" do
|
286
|
+
it "has helper functions" do
|
287
|
+
ZFS('tank').type.should eq :filesystem
|
288
|
+
end
|
289
|
+
|
290
|
+
it "gets correct types" do
|
291
|
+
ZFS('tank/foo').create
|
292
|
+
ZFS('tank/foo').exec?.should be_true
|
293
|
+
ZFS('tank/foo').origin.should be_nil
|
294
|
+
ZFS('tank/foo').creation.should be_an_instance_of DateTime
|
295
|
+
ZFS('tank/foo').referenced.should be_an_instance_of Fixnum
|
296
|
+
ZFS('tank/foo').mountpoint.should eq Pathname('/tank/foo')
|
297
|
+
|
298
|
+
ZFS('tank/foo').destroy!
|
299
|
+
end
|
300
|
+
|
301
|
+
it "sets correct types" do
|
302
|
+
ZFS('tank/foo').create
|
303
|
+
ZFS('tank/foo').exec?.should be_true
|
304
|
+
ZFS('tank/foo').exec = false
|
305
|
+
ZFS('tank/foo').exec?.should be_false
|
306
|
+
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
# Now that we've tested properties, we should be able to test fetching by mountpoint
|
312
|
+
describe "ZFS()" do
|
313
|
+
include_context "vagrant"
|
314
|
+
|
315
|
+
it "takes a mountpoint as argument" do
|
316
|
+
ZFS('/tank').should eq ZFS('tank')
|
317
|
+
ZFS('/tank').name.should eq 'tank'
|
318
|
+
ZFS('tank/foo').create
|
319
|
+
ZFS('/tank/foo').should eq ZFS('tank/foo')
|
320
|
+
ZFS('tank/foo').destroy!
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
describe ZFS::Snapshot do
|
325
|
+
include_context "vagrant"
|
326
|
+
|
327
|
+
describe "#clone" do
|
328
|
+
it "raises an error when target already exists" do
|
329
|
+
snapshot = ZFS('tank/foo').create.snapshot('foo')
|
330
|
+
ZFS('tank/bar').create
|
331
|
+
|
332
|
+
expect { snapshot.clone!('tank/bar') }.to raise_error ZFS::AlreadyExists
|
333
|
+
|
334
|
+
ZFS('tank/foo').destroy!(children: true)
|
335
|
+
ZFS('tank/bar').destroy!
|
336
|
+
end
|
337
|
+
|
338
|
+
it "clones a snapshot to a filesystem" do
|
339
|
+
snapshot = ZFS('tank/foo').create.snapshot('foo')
|
340
|
+
|
341
|
+
fs = snapshot.clone!('tank/bar')
|
342
|
+
fs.should be_an_instance_of ZFS::Filesystem
|
343
|
+
fs.should exist
|
344
|
+
fs.should eq ZFS('tank/bar')
|
345
|
+
|
346
|
+
fs.destroy!
|
347
|
+
snapshot.destroy!
|
348
|
+
ZFS('tank/foo').destroy!
|
349
|
+
end
|
350
|
+
|
351
|
+
it "creates parent-filesystems when requested" do
|
352
|
+
snapshot = ZFS('tank/foo').create.snapshot('foo')
|
353
|
+
|
354
|
+
fs = snapshot.clone!('tank/bar/baz', parents: true)
|
355
|
+
fs.should be_an_instance_of ZFS::Filesystem
|
356
|
+
fs.should exist
|
357
|
+
fs.should eq ZFS('tank/bar/baz')
|
358
|
+
|
359
|
+
fs.destroy!
|
360
|
+
snapshot.destroy!
|
361
|
+
ZFS('tank/foo').destroy!
|
362
|
+
ZFS('tank/bar').destroy!(children: true)
|
363
|
+
end
|
364
|
+
|
365
|
+
it "returns a filesystem with a valid 'origin' property" do
|
366
|
+
snapshot = ZFS('tank/foo').create.snapshot('foo')
|
367
|
+
|
368
|
+
fs = snapshot.clone!('tank/bar')
|
369
|
+
fs.should be_an_instance_of ZFS::Filesystem
|
370
|
+
fs.origin.should eq snapshot
|
371
|
+
|
372
|
+
fs.destroy!
|
373
|
+
snapshot.destroy!
|
374
|
+
ZFS('tank/foo').destroy!
|
375
|
+
end
|
376
|
+
|
377
|
+
it "accepts a ZFS as a valid destination" do
|
378
|
+
snapshot = ZFS('tank/foo').create.snapshot('foo')
|
379
|
+
|
380
|
+
fs = snapshot.clone!(ZFS('tank/bar'))
|
381
|
+
fs.should be_an_instance_of ZFS::Filesystem
|
382
|
+
fs.origin.should eq snapshot
|
383
|
+
|
384
|
+
fs.destroy!
|
385
|
+
snapshot.destroy!
|
386
|
+
ZFS('tank/foo').destroy!
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
describe "#send_to" do
|
391
|
+
before(:all) do
|
392
|
+
@source = ZFS('tank/foo').create
|
393
|
+
@dest1 = ZFS('tank/bar').create
|
394
|
+
@dest2 = ZFS('tank/baz')
|
395
|
+
@sourcesnap = @source.snapshot('snapshot')
|
396
|
+
end
|
397
|
+
|
398
|
+
after(:all) do
|
399
|
+
@source.destroy!(children: true)
|
400
|
+
@dest1.destroy!(children: true)
|
401
|
+
@dest2.destroy!(children: true) if @dest2.exist?
|
402
|
+
end
|
403
|
+
|
404
|
+
it "sends the snapshot to another filesystem" do
|
405
|
+
@sourcesnap.send_to(@dest2)
|
406
|
+
(@dest2 + '@snapshot').should exist
|
407
|
+
@dest2.snapshots.should eq [(@dest2 + '@snapshot')]
|
408
|
+
|
409
|
+
@dest2.destroy!(children: true)
|
410
|
+
end
|
411
|
+
|
412
|
+
it "raises an error if the destination filesystem exists when sending a full stream" do
|
413
|
+
expect { @sourcesnap.send_to(@dest1) }.to raise_error ZFS::AlreadyExists
|
414
|
+
end
|
415
|
+
|
416
|
+
it "supports incremental/intermediary snapshots" do
|
417
|
+
@sourcesnap.send_to(@dest2)
|
418
|
+
|
419
|
+
snap2 = @source.snapshot('snapshot2')
|
420
|
+
snap2.send_to(@dest2, incremental: @sourcesnap)
|
421
|
+
snap2.should exist
|
422
|
+
(@dest2 + '@snapshot2').should exist
|
423
|
+
|
424
|
+
snap3 = @source.snapshot('snapshot3')
|
425
|
+
snap3.send_to(@dest2, intermediary: @sourcesnap)
|
426
|
+
snap3.should exist
|
427
|
+
(@dest2 + '@snapshot3').should exist
|
428
|
+
|
429
|
+
snap3.destroy!
|
430
|
+
snap2.destroy!
|
431
|
+
@dest2.destroy!(children: true)
|
432
|
+
end
|
433
|
+
|
434
|
+
it "raises an error when specifying invalid combinations of options" do
|
435
|
+
expect { @sourcesnap.send_to(@dest1, incremental: @sourcesnap, intermediary: @sourcesnap) }.to raise_error ArgumentError
|
436
|
+
end
|
437
|
+
|
438
|
+
it "supports relative paths (Strings beginning with @) as incremental sources" do
|
439
|
+
@sourcesnap.send_to(@dest2)
|
440
|
+
|
441
|
+
snap2 = @source.snapshot('snapshot2')
|
442
|
+
snap2.send_to(@dest2, incremental: '@snapshot')
|
443
|
+
snap2.should exist
|
444
|
+
(@dest2 + '@snapshot2').should exist
|
445
|
+
|
446
|
+
snap3 = @source.snapshot('snapshot3')
|
447
|
+
snap3.send_to(@dest2, intermediary: '@snapshot')
|
448
|
+
snap3.should exist
|
449
|
+
(@dest2 + '@snapshot3').should exist
|
450
|
+
|
451
|
+
snap3.destroy!
|
452
|
+
snap2.destroy!
|
453
|
+
@dest2.destroy!(children: true)
|
454
|
+
end
|
455
|
+
|
456
|
+
it "raises an error if incremental snapshot isn't in the same filesystem as source" do
|
457
|
+
s = @dest1.snapshot('foo')
|
458
|
+
expect { @sourcesnap.send_to(@dest1, incremental: s) }.to raise_error ArgumentError
|
459
|
+
s.destroy!
|
460
|
+
end
|
461
|
+
|
462
|
+
it "raises an error when filesystems/snapshots don't exist" do
|
463
|
+
expect { @sourcesnap.send_to(@dest1, incremental: ZFS('tank/foo@none')) }.to raise_error ZFS::NotFound
|
464
|
+
expect { @sourcesnap.send_to(@dest1, intermediary: ZFS('tank/foo@none')) }.to raise_error ZFS::NotFound
|
465
|
+
expect { @sourcesnap.send_to(@dest2, intermediary: @sourcesnap) }.to raise_error ZFS::NotFound
|
466
|
+
end
|
467
|
+
|
468
|
+
it "supports replication streams" do
|
469
|
+
source2 = (@source + 'sub').create
|
470
|
+
source2.snapshot('snapshot')
|
471
|
+
|
472
|
+
@sourcesnap.send_to(@dest2, replication: true)
|
473
|
+
|
474
|
+
@dest2.should exist
|
475
|
+
(@dest2 + 'sub@snapshot').should exist
|
476
|
+
|
477
|
+
@dest2.destroy!(children: true)
|
478
|
+
end
|
479
|
+
|
480
|
+
it "supports 'receive -d'" do
|
481
|
+
@sourcesnap.send_to(@dest1, use_sent_name: true)
|
482
|
+
(@dest1 + 'foo').should exist
|
483
|
+
(@dest1 + 'foo@snapshot').should exist
|
484
|
+
(@dest1 + 'foo').destroy!(children: true)
|
485
|
+
end
|
486
|
+
|
487
|
+
it "raises an error when using 'receive -d' and destination is missing" do
|
488
|
+
expect { @sourcesnap.send_to(@dest2, use_sent_name: true) }.to raise_error ZFS::NotFound
|
489
|
+
end
|
490
|
+
end
|
491
|
+
end
|
492
|
+
|
493
|
+
describe ZFS::Filesystem do
|
494
|
+
include_context "vagrant"
|
495
|
+
|
496
|
+
describe "#promote!" do
|
497
|
+
it "promotes a clone" do
|
498
|
+
snapshot = ZFS('tank/foo').create.snapshot('foo')
|
499
|
+
|
500
|
+
fs = snapshot.clone!('tank/bar')
|
501
|
+
fs.origin.should eq snapshot
|
502
|
+
|
503
|
+
fs.promote!
|
504
|
+
|
505
|
+
fs.origin.should be_nil
|
506
|
+
snapshot.parent.origin.should eq fs + '@foo'
|
507
|
+
|
508
|
+
snapshot.parent.destroy!
|
509
|
+
fs.snapshots.first.destroy!
|
510
|
+
fs.destroy!
|
511
|
+
end
|
512
|
+
|
513
|
+
it "raises an error if filesystem is not a clone" do
|
514
|
+
expect { ZFS('tank/foo').create.promote! }.to raise_error ZFS::NotFound
|
515
|
+
ZFS('tank/foo').destroy!
|
516
|
+
end
|
517
|
+
end
|
518
|
+
end
|
metadata
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: zfs
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- kvs
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-03-21 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: &70251194879920 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 2.8.0
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70251194879920
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: guard-rspec
|
27
|
+
requirement: &70251194879320 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70251194879320
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: guard-bundler
|
38
|
+
requirement: &70251194878680 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70251194878680
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rake
|
49
|
+
requirement: &70251194878160 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70251194878160
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: open4
|
60
|
+
requirement: &70251199214680 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
type: :runtime
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *70251199214680
|
69
|
+
description: Makes it possible to query and manipulate ZFS filesystems, snapshots,
|
70
|
+
etc.
|
71
|
+
email:
|
72
|
+
- kvs@binarysolutions.dk
|
73
|
+
executables: []
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- .gitignore
|
78
|
+
- Gemfile
|
79
|
+
- Guardfile
|
80
|
+
- LICENSE
|
81
|
+
- README.md
|
82
|
+
- Vagrantfile
|
83
|
+
- lib/zfs.rb
|
84
|
+
- spec/spec_helper.rb
|
85
|
+
- spec/zfs_spec.rb
|
86
|
+
homepage: https://github.com/kvs/ruby-zfs
|
87
|
+
licenses: []
|
88
|
+
post_install_message:
|
89
|
+
rdoc_options: []
|
90
|
+
require_paths:
|
91
|
+
- lib
|
92
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
93
|
+
none: false
|
94
|
+
requirements:
|
95
|
+
- - ! '>='
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0'
|
98
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
99
|
+
none: false
|
100
|
+
requirements:
|
101
|
+
- - ! '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
requirements: []
|
105
|
+
rubyforge_project:
|
106
|
+
rubygems_version: 1.8.11
|
107
|
+
signing_key:
|
108
|
+
specification_version: 3
|
109
|
+
summary: An library for interacting with ZFS
|
110
|
+
test_files:
|
111
|
+
- spec/spec_helper.rb
|
112
|
+
- spec/zfs_spec.rb
|