zfs 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ .bundle
2
+ .vagrant
3
+ Gemfile.lock
4
+ test.rb
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
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
@@ -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