diff options
| author | Andrew Shafer <andrew@reductivelabs.com> | 2008-11-30 23:54:57 -0700 |
|---|---|---|
| committer | James Turnbull <james@lovedthanlost.net> | 2008-12-01 18:35:16 +1100 |
| commit | 0a40668b348d210cb6cb9e9c25320ccc981ae422 (patch) | |
| tree | 0008a4efde25ec8459d31131731c8ee675068485 | |
| parent | 047e5d073c2362da95553c1778b9eb5176b0c05b (diff) | |
| download | puppet-0a40668b348d210cb6cb9e9c25320ccc981ae422.tar.gz puppet-0a40668b348d210cb6cb9e9c25320ccc981ae422.tar.xz puppet-0a40668b348d210cb6cb9e9c25320ccc981ae422.zip | |
Feature #1783 - Add ZFS support
Types and providers to manage zfs and zpool
| -rw-r--r-- | lib/puppet/provider/zfs/solaris.rb | 56 | ||||
| -rw-r--r-- | lib/puppet/provider/zpool/solaris.rb | 112 | ||||
| -rwxr-xr-x | lib/puppet/type/zfs.rb | 45 | ||||
| -rwxr-xr-x | lib/puppet/type/zpool.rb | 60 | ||||
| -rwxr-xr-x | spec/unit/provider/zfs/solaris.rb | 87 | ||||
| -rwxr-xr-x | spec/unit/provider/zpool/solaris.rb | 158 | ||||
| -rwxr-xr-x | spec/unit/type/zfs.rb | 28 | ||||
| -rwxr-xr-x | spec/unit/type/zpool.rb | 28 |
8 files changed, 574 insertions, 0 deletions
diff --git a/lib/puppet/provider/zfs/solaris.rb b/lib/puppet/provider/zfs/solaris.rb new file mode 100644 index 000000000..4d382cfad --- /dev/null +++ b/lib/puppet/provider/zfs/solaris.rb @@ -0,0 +1,56 @@ +Puppet::Type.type(:zfs).provide(:solaris) do + desc "Provider for Solaris zfs." + + commands :zfs => "/usr/sbin/zfs" + defaultfor :operatingsystem => :solaris + + def add_properties + properties = [] + Puppet::Type.type(:zfs).validproperties.each do |property| + next if property == :ensure + if value = @resource[property] and value != "" + properties << "-o" << "#{property}=#{value}" + end + end + properties + end + + def arrayify_second_line_on_whitespace(text) + if second_line = text.split("\n")[1] + second_line.split("\s") + else + [] + end + end + + def create + zfs *([:create] + add_properties + [@resource[:name]]) + end + + def delete + zfs(:destroy, @resource[:name]) + end + + def exists? + if zfs(:list).split("\n").detect { |line| line.split("\s")[0] == @resource[:name] } + true + else + false + end + end + + [:mountpoint, :compression, :copies, :quota, :reservation, :sharenfs, :snapdir].each do |field| + define_method(field) do + #special knowledge of format + #the command returns values in this format with the header + #NAME PROPERTY VALUE SOURCE + arrayify_second_line_on_whitespace(zfs(:get, field, @resource[:name]))[2] + end + + define_method(field.to_s + "=") do |should| + zfs(:set, "#{field}=#{should}", @resource[:name]) + end + end + +end + diff --git a/lib/puppet/provider/zpool/solaris.rb b/lib/puppet/provider/zpool/solaris.rb new file mode 100644 index 000000000..d680a5f63 --- /dev/null +++ b/lib/puppet/provider/zpool/solaris.rb @@ -0,0 +1,112 @@ +Puppet::Type.type(:zpool).provide(:solaris) do + desc "Provider for Solaris zpool." + + commands :zpool => "/usr/sbin/zpool" + defaultfor :operatingsystem => :solaris + + def process_zpool_data(pool_array) + if pool_array == [] + return Hash.new(:absent) + end + #get the name and get rid of it + pool = Hash.new([]) + pool[:pool] = pool_array[0] + pool_array.shift + + #order matters here :( + tmp = [] + + pool_array.reverse.each_with_index do |value, i| + case value + when "spares": pool[:spare] = tmp.reverse and tmp.clear + when "logs": pool[:log] = tmp.reverse and tmp.clear + when "mirror", "raidz1", "raidz2": + sym = value == "mirror" ? :mirror : :raidz + pool[sym].unshift(tmp.reverse.join(' ')) + pool[:raid_parity] = "raidz2" if value == "raidz2" + tmp.clear + else + tmp << value + pool[:disk] = tmp.reverse if i == 0 + end + end + + pool + end + + def get_pool_data + #this is all voodoo dependent on the output from zpool + zpool_data = %x{ zpool status #{@resource[:pool]}}.split("\n").select { |line| line.index("\t") == 0 }.collect { |l| l.strip.split("\s")[0] } + zpool_data.shift + zpool_data + end + + def current_pool + unless (defined?(@current_pool) and @current_pool) + @current_pool = process_zpool_data(get_pool_data) + end + @current_pool + end + + def flush + @current_pool= nil + end + + #Adds log and spare + def build_named(name) + if prop = @resource[name.intern] + [name] + prop.collect { |p| p.split(' ') }.flatten + else + [] + end + end + + #query for parity and set the right string + def raidzarity + @resource[:raid_parity] ? @resource[:raid_parity] : "raidz1" + end + + #handle mirror or raid + def handle_multi_arrays(prefix, array) + array.collect{ |a| [prefix] + a.split(' ') }.flatten + end + + #builds up the vdevs for create command + def build_vdevs + if disk = @resource[:disk] + disk.collect { |d| d.split(' ') }.flatten + elsif mirror = @resource[:mirror] + handle_multi_arrays("mirror", mirror) + elsif raidz = @resource[:raidz] + handle_multi_arrays(raidzarity, raidz) + end + end + + def create + zpool(*([:create, @resource[:pool]] + build_vdevs + build_named("spare") + build_named("log"))) + end + + def delete + zpool :destroy, @resource[:pool] + end + + def exists? + if current_pool[:pool] == :absent + false + else + true + end + end + + [:disk, :mirror, :raidz, :log, :spare].each do |field| + define_method(field) do + current_pool[field] + end + + define_method(field.to_s + "=") do |should| + Puppet.warning "NO CHANGES BEING MADE: zpool %s does not match, should be '%s' currently is '%s'" % [field, should, current_pool[field]] + end + end + +end + diff --git a/lib/puppet/type/zfs.rb b/lib/puppet/type/zfs.rb new file mode 100755 index 000000000..d3af3a461 --- /dev/null +++ b/lib/puppet/type/zfs.rb @@ -0,0 +1,45 @@ +module Puppet + newtype(:zfs) do + @doc = "Manage zfs. Create destroy and set properties on zfs instances." + + ensurable + + newparam(:name) do + desc "The full name for this filesystem. (including the zpool)" + end + + newproperty(:mountpoint) do + desc "The mountpoint property." + end + + newproperty(:compression) do + desc "The compression property." + end + + newproperty(:copies) do + desc "The copies property." + end + + newproperty(:quota) do + desc "The quota property." + end + + newproperty(:reservation) do + desc "The reservation property." + end + + newproperty(:sharenfs) do + desc "The sharenfs property." + end + + newproperty(:snapdir) do + desc "The sharenfs property." + end + + autorequire(:zpool) do + #strip the zpool off the zfs name and autorequire it + [@parameters[:name].value.split('/')[0]] + end + end +end + diff --git a/lib/puppet/type/zpool.rb b/lib/puppet/type/zpool.rb new file mode 100755 index 000000000..6d589a0fe --- /dev/null +++ b/lib/puppet/type/zpool.rb @@ -0,0 +1,60 @@ +module Puppet + newtype(:zpool) do + @doc = "Manage zpools. Create and delete zpools. The provider WILL NOT SYNC, only report differences. + + Supports vdevs with mirrors, raidz, logs and spares." + + ensurable + + newproperty(:disk, :array_matching => :all) do + desc "The disk(s) for this pool. Can be an array or space separated string" + end + + newproperty(:mirror, :array_matching => :all) do + desc "List of all the devices to mirror for this pool. Each mirror should be a space separated string. + mirror => [\"disk1 disk2\", \"disk3 disk4\"]" + + validate do |value| + if value.include?(",") + raise ArgumentError, "mirror names must be provided as string separated, not a comma-separated list" + end + end + end + + newproperty(:raidz, :array_matching => :all) do + desc "List of all the devices to raid for this pool. Should be an array of space separated strings. + raidz => [\"disk1 disk2\", \"disk3 disk4\"]" + + validate do |value| + if value.include?(",") + raise ArgumentError, "raid names must be provided as string separated, not a comma-separated list" + end + end + end + + newproperty(:spare, :array_matching => :all) do + desc "Spare disk(s) for this pool." + end + + newproperty(:log, :array_matching => :all) do + desc "Log disks for this pool. (doesn't support mirroring yet)" + end + + newparam(:pool) do + desc "The name for this pool." + isnamevar + end + + newparam(:raid_parity) do + desc "Determines parity when using raidz property." + end + + validate do + has_should = [:disk, :mirror, :raidz].select { |prop| self.should(prop) } + if has_should.length > 1 + self.fail "You cannot specify %s on this type (only one)" % has_should.join(" and ") + end + end + end +end + diff --git a/spec/unit/provider/zfs/solaris.rb b/spec/unit/provider/zfs/solaris.rb new file mode 100755 index 000000000..63aefcdc4 --- /dev/null +++ b/spec/unit/provider/zfs/solaris.rb @@ -0,0 +1,87 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../../spec_helper' + +provider_class = Puppet::Type.type(:zfs).provider(:solaris) + +describe provider_class do + before do + @resource = stub("resource", :name => "myzfs") + @resource.stubs(:[]).with(:name).returns "myzfs" + @resource.stubs(:[]).returns "shouldvalue" + @provider = provider_class.new(@resource) + end + + describe "when calling add_properties" do + it "should add -o and the key=value for each properties with a value" do + @resource.stubs(:[]).with(:quota).returns "" + @resource.stubs(:[]).with(:mountpoint).returns "/foo" + properties = @provider.add_properties + properties.include?("-o").should == true + properties.include?("mountpoint=/foo").should == true + properties.detect { |a| a.include?("quota") }.should == nil + end + end + + describe "when calling create" do + it "should call add_properties" do + @provider.stubs(:zfs) + @provider.expects(:add_properties).returns([]) + @provider.create + end + + it "should call zfs with create, properties and this zfs" do + @provider.stubs(:add_properties).returns(%w{a b}) + @provider.expects(:zfs).with(:create, "a", "b", @resource[:name]) + @provider.create + end + end + + describe "when calling delete" do + it "should call zfs with :destroy and this zfs" do + @provider.expects(:zfs).with(:destroy, @resource[:name]) + @provider.delete + end + end + + describe "when calling exist?" do + it "should call zfs with :list" do + #return stuff because we have to slice and dice it + @provider.expects(:zfs).with(:list).returns("NAME USED AVAIL REFER MOUNTPOINT\nmyzfs 100K 27.4M /myzfs") + @provider.exists? + end + + it "should return true if returned values match the name" do + @provider.stubs(:zfs).with(:list).returns("NAME USED AVAIL REFER MOUNTPOINT\n#{@resource[:name]} 100K 27.4M /myzfs") + @provider.exists?.should == true + end + + it "should return false if returned values don't match the name" do + @provider.stubs(:zfs).with(:list).returns("no soup for you") + @provider.exists?.should == false + end + + end + + [:mountpoint, :compression, :copies, :quota, :reservation, :sharenfs, :snapdir].each do |prop| + describe "when getting the #{prop} value" do + it "should call zfs with :get, #{prop} and this zfs" do + @provider.expects(:zfs).with(:get, prop, @resource[:name]).returns("NAME PROPERTY VALUE SOURCE\nmyzfs name value blah") + @provider.send(prop) + end + + it "should get the third value of the second line from the output" do + @provider.stubs(:zfs).with(:get, prop, @resource[:name]).returns("NAME PROPERTY VALUE SOURCE\nmyzfs name value blah") + @provider.send(prop).should == "value" + end + end + + describe "when setting the #{prop} value" do + it "should call zfs with :set, #{prop}=value and this zfs" do + @provider.expects(:zfs).with(:set, "#{prop}=value", @resource[:name]) + @provider.send("#{prop}=".intern, "value") + end + end + end + +end diff --git a/spec/unit/provider/zpool/solaris.rb b/spec/unit/provider/zpool/solaris.rb new file mode 100755 index 000000000..af4db88c1 --- /dev/null +++ b/spec/unit/provider/zpool/solaris.rb @@ -0,0 +1,158 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../../spec_helper' + +provider_class = Puppet::Type.type(:zpool).provider(:solaris) + +describe provider_class do + before do + @resource = stub("resource", :name => "mypool") + @resource.stubs(:[]).returns "shouldvalue" + @provider = provider_class.new(@resource) + end + + describe "when getting the instance" do + it "should call process_zpool_data with the result of get_pool_data only once" do + @provider.stubs(:get_pool_data).returns(["foo", "disk"]) + @provider.expects(:process_zpool_data).with(["foo", "disk"]).returns("stuff").once + @provider.current_pool + @provider.current_pool + end + end + + describe "when calling flush" do + it "should need to reload the pool" do + @provider.stubs(:get_pool_data) + @provider.expects(:process_zpool_data).returns("stuff").times(2) + @provider.current_pool + @provider.flush + @provider.current_pool + end + end + + describe "when procesing zpool data" do + before do + @zpool_data = ["foo", "disk"] + end + + describe "when there is no data" do + it "should return a hash with ensure=>:absent" do + @provider.process_zpool_data([])[:ensure].should == :absent + end + end + + describe "when there is a spare" do + it "should add the spare disk to the hash and strip the array" do + @zpool_data += ["spares", "spare_disk"] + @provider.process_zpool_data(@zpool_data)[:spare].should == ["spare_disk"] + end + end + + describe "when there is a log" do + it "should add the log disk to the hash and strip the array" do + @zpool_data += ["logs", "log_disk"] + @provider.process_zpool_data(@zpool_data)[:log].should == ["log_disk"] + end + end + + describe "when the vdev is a mirror" do + it "should call create_multi_array with mirror" do + @zpool_data = ["mirrorpool", "mirror", "disk1", "disk2", "mirror", "disk3", "disk4"] + @provider.process_zpool_data(@zpool_data)[:mirror].should == ["disk1 disk2", "disk3 disk4"] + end + end + + describe "when the vdev is a raidz1" do + it "should call create_multi_array with raidz1" do + @zpool_data = ["mirrorpool", "raidz1", "disk1", "disk2"] + @provider.process_zpool_data(@zpool_data)[:raidz].should == ["disk1 disk2"] + end + end + + describe "when the vdev is a raidz2" do + it "should call create_multi_array with raidz2 and set the raid_parity" do + @zpool_data = ["mirrorpool", "raidz2", "disk1", "disk2"] + pool = @provider.process_zpool_data(@zpool_data) + pool[:raidz].should == ["disk1 disk2"] + pool[:raid_parity].should == "raidz2" + end + end + end + + describe "when calling the getters and setters" do + [:disk, :mirror, :raidz, :log, :spare].each do |field| + describe "when calling %s" % field do + it "should get the %s value from the current_pool hash" % field do + pool_hash = mock "pool hash" + pool_hash.expects(:[]).with(field) + @provider.stubs(:current_pool).returns(pool_hash) + @provider.send(field) + end + end + + describe "when setting the %s" % field do + it "should warn the %s values were not in sync" % field do + Puppet.expects(:warning).with("NO CHANGES BEING MADE: zpool %s does not match, should be 'shouldvalue' currently is 'currentvalue'" % field) + @provider.stubs(:current_pool).returns(Hash.new("currentvalue")) + @provider.send((field.to_s + "=").intern, "shouldvalue") + end + end + end + end + + describe "when calling create" do + before do + @resource.stubs(:[]).with(:pool).returns("mypool") + @provider.stubs(:zpool) + end + + + it "should call build_vdevs" do + @provider.expects(:build_vdevs).returns([]) + @provider.create + end + + it "should call build_named with 'spares' and 'log" do + @provider.expects(:build_named).with("spare").returns([]) + @provider.expects(:build_named).with("log").returns([]) + @provider.create + end + + it "should call zpool with arguments from build_vdevs and build_named" do + @provider.expects(:zpool).with(:create, 'mypool', 'shouldvalue', 'spare', 'shouldvalue', 'log', 'shouldvalue') + @provider.create + end + end + + describe "when calling delete" do + it "should call zpool with destroy and the pool name" do + @resource.stubs(:[]).with(:pool).returns("poolname") + @provider.expects(:zpool).with(:destroy, "poolname") + @provider.delete + end + end + + describe "when calling exists?" do + before do + @current_pool = Hash.new(:absent) + @provider.stubs(:get_pool_data).returns([]) + @provider.stubs(:process_zpool_data).returns(@current_pool) + end + + it "should get the current pool" do + @provider.expects(:process_zpool_data).returns(@current_pool) + @provider.exists? + end + + it "should return false if the current_pool is absent" do + #the before sets it up + @provider.exists?.should == false + end + + it "should return true if the current_pool has values" do + @current_pool[:pool] = "mypool" + @provider.exists?.should == true + end + end + +end diff --git a/spec/unit/type/zfs.rb b/spec/unit/type/zfs.rb new file mode 100755 index 000000000..434415e24 --- /dev/null +++ b/spec/unit/type/zfs.rb @@ -0,0 +1,28 @@ +#!/usr/bin/env ruby + +Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } + +zpool = Puppet::Type.type(:zfs) + +describe zpool do + before do + @provider = stub 'provider' + @resource = stub 'resource', :resource => nil, :provider => @provider, :line => nil, :file => nil + end + + properties = [:ensure, :mountpoint, :compression, :copies, :quota, :reservation, :sharenfs, :snapdir] + + properties.each do |property| + it "should have a %s property" % property do + zpool.attrclass(property).ancestors.should be_include(Puppet::Property) + end + end + + parameters = [:name] + + parameters.each do |parameter| + it "should have a %s parameter" % parameter do + zpool.attrclass(parameter).ancestors.should be_include(Puppet::Parameter) + end + end +end diff --git a/spec/unit/type/zpool.rb b/spec/unit/type/zpool.rb new file mode 100755 index 000000000..6477d061d --- /dev/null +++ b/spec/unit/type/zpool.rb @@ -0,0 +1,28 @@ +#!/usr/bin/env ruby + +Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } + +zpool = Puppet::Type.type(:zpool) + +describe zpool do + before do + @provider = stub 'provider' + @resource = stub 'resource', :resource => nil, :provider => @provider, :line => nil, :file => nil + end + + properties = [:ensure, :disk, :mirror, :raidz, :spare, :log] + + properties.each do |property| + it "should have a %s property" % property do + zpool.attrclass(property).ancestors.should be_include(Puppet::Property) + end + end + + parameters = [:pool, :raid_parity] + + parameters.each do |parameter| + it "should have a %s parameter" % parameter do + zpool.attrclass(parameter).ancestors.should be_include(Puppet::Parameter) + end + end +end |
