diff --git a/lib/puppet/provider/zfs/zfs.rb b/lib/puppet/provider/zfs/zfs.rb index 74b3aac..8e85005 100644 --- a/lib/puppet/provider/zfs/zfs.rb +++ b/lib/puppet/provider/zfs/zfs.rb @@ -1,104 +1,109 @@ Puppet::Type.type(:zfs).provide(:zfs) do desc 'Provider for zfs.' commands zfs: 'zfs' def self.instances zfs(:list, '-H').split("\n").map do |line| name, _used, _avail, _refer, _mountpoint = line.split(%r{\s+}) new(name: name, ensure: :present) end end def add_properties properties = [] Puppet::Type.type(:zfs).validproperties.each do |property| next if property == :ensure if (value = @resource[property]) && value != '' if property == :volsize properties << '-V' << value.to_s else properties << '-o' << "#{property}=#{value}" end end end properties end def create zfs(*([:create] + add_properties + [@resource[:name]])) end def destroy zfs(:destroy, @resource[:name]) end def exists? zfs(:list, @resource[:name]) true rescue Puppet::ExecutionFailure false end # On FreeBSD zoned is called jailed def container_property case Facter.value(:operatingsystem) when 'FreeBSD' :jailed else :zoned end end PARAMETER_UNSET_OR_NOT_AVAILABLE = '-'.freeze # https://docs.oracle.com/cd/E19963-01/html/821-1448/gbscy.html # shareiscsi (added in build 120) was removed from S11 build 136 # aclmode was removed from S11 in build 139 but it may have been added back # acltype is for ZFS on Linux, and allows disabling or enabling POSIX ACLs # http://webcache.googleusercontent.com/search?q=cache:-p74K0DVsdwJ:developers.slashdot.org/story/11/11/09/2343258/solaris-11-released+&cd=13 [:aclmode, :acltype, :shareiscsi].each do |field| # The zfs commands use the property value '-' to indicate that the # property is not set. We make use of this value to indicate that the # property is not set since it is not available. Conversely, if these # properties are attempted to be unset, and resulted in an error, our # best bet is to catch the exception and continue. define_method(field) do begin zfs(:get, '-H', '-o', 'value', field, @resource[:name]).strip rescue PARAMETER_UNSET_OR_NOT_AVAILABLE end end define_method(field.to_s + '=') do |should| begin zfs(:set, "#{field}=#{should}", @resource[:name]) rescue PARAMETER_UNSET_OR_NOT_AVAILABLE end end end - [:aclinherit, :atime, :canmount, :checksum, - :compression, :copies, :dedup, :devices, :exec, :logbias, - :mountpoint, :nbmand, :primarycache, :quota, :readonly, - :recordsize, :refquota, :refreservation, :reservation, - :secondarycache, :setuid, :sharenfs, :sharesmb, - :snapdir, :version, :volsize, :vscan, :xattr].each do |field| + zfs_properties = [:aclinherit, :atime, :canmount, :checksum, + :compression, :copies, :dedup, :devices, :exec, :logbias, + :mountpoint, :nbmand, :primarycache, :quota, :readonly, + :recordsize, :refquota, :refreservation, :reservation, + :secondarycache, :setuid, :sharenfs, :sharesmb, + :snapdir, :version, :volsize, :vscan, :xattr] + + # The overlay property is only supported on Linux + zfs_properties << :overlay if Facter.value(:kernel) == 'Linux' + + zfs_properties.each do |field| define_method(field) do zfs(:get, '-H', '-o', 'value', field, @resource[:name]).strip end define_method(field.to_s + '=') do |should| zfs(:set, "#{field}=#{should}", @resource[:name]) end end define_method(:zoned) do zfs(:get, '-H', '-o', 'value', container_property, @resource[:name]).strip end define_method('zoned=') do |should| zfs(:set, "#{container_property}=#{should}", @resource[:name]) end end diff --git a/lib/puppet/type/zfs.rb b/lib/puppet/type/zfs.rb index 36c8f8b..520f317 100644 --- a/lib/puppet/type/zfs.rb +++ b/lib/puppet/type/zfs.rb @@ -1,162 +1,168 @@ # Manage zfs. Create destroy and set properties on zfs instances. module Puppet Type.newtype(:zfs) do desc <<-DESC Manage zfs. Create destroy and set properties on zfs instances. **Autorequires:** If Puppet is managing the zpool at the root of this zfs instance, the zfs resource will autorequire it. If Puppet is managing any parent zfs instances, the zfs resource will autorequire them. @example Using zfs. zfs { 'tstpool': ensure => present, } DESC ensurable newparam(:name) do desc 'The full name for this filesystem (including the zpool).' end newproperty(:aclinherit) do desc 'The aclinherit property. Valid values are `discard`, `noallow`, `restricted`, `passthrough`, `passthrough-x`.' end newproperty(:aclmode) do desc 'The aclmode property. Valid values are `discard`, `groupmask`, `passthrough`.' end newproperty(:acltype) do desc "The acltype propery. Valid values are 'noacl' and 'posixacl'. Only supported on Linux." end newproperty(:atime) do desc 'The atime property. Valid values are `on`, `off`.' end newproperty(:canmount) do desc 'The canmount property. Valid values are `on`, `off`, `noauto`.' end newproperty(:checksum) do desc 'The checksum property. Valid values are `on`, `off`, `fletcher2`, `fletcher4`, `sha256`.' end newproperty(:compression) do desc 'The compression property. Valid values are `on`, `off`, `lzjb`, `gzip`, `gzip-[1-9]`, `zle`.' end newproperty(:copies) do desc 'The copies property. Valid values are `1`, `2`, `3`.' end newproperty(:dedup) do desc 'The dedup property. Valid values are `on`, `off`.' end newproperty(:devices) do desc 'The devices property. Valid values are `on`, `off`.' end newproperty(:exec) do desc 'The exec property. Valid values are `on`, `off`.' end newproperty(:logbias) do desc 'The logbias property. Valid values are `latency`, `throughput`.' end newproperty(:mountpoint) do desc 'The mountpoint property. Valid values are ``, `legacy`, `none`.' end newproperty(:nbmand) do desc 'The nbmand property. Valid values are `on`, `off`.' end + if Facter.value(:kernel) == 'Linux' + newproperty(:overlay) do + desc 'The overlay property. Valid values are `on`, `off`.' + end + end + newproperty(:primarycache) do desc 'The primarycache property. Valid values are `all`, `none`, `metadata`.' end newproperty(:quota) do desc 'The quota property. Valid values are ``, `none`.' end newproperty(:readonly) do desc 'The readonly property. Valid values are `on`, `off`.' end newproperty(:recordsize) do desc 'The recordsize property. Valid values are powers of two between 512 and 128k.' end newproperty(:refquota) do desc 'The refquota property. Valid values are ``, `none`.' end newproperty(:refreservation) do desc 'The refreservation property. Valid values are ``, `none`.' end newproperty(:reservation) do desc 'The reservation property. Valid values are ``, `none`.' end newproperty(:secondarycache) do desc 'The secondarycache property. Valid values are `all`, `none`, `metadata`.' end newproperty(:setuid) do desc 'The setuid property. Valid values are `on`, `off`.' end newproperty(:shareiscsi) do desc 'The shareiscsi property. Valid values are `on`, `off`, `type=`.' end newproperty(:sharenfs) do desc 'The sharenfs property. Valid values are `on`, `off`, share(1M) options' end newproperty(:sharesmb) do desc 'The sharesmb property. Valid values are `on`, `off`, sharemgr(1M) options' end newproperty(:snapdir) do desc 'The snapdir property. Valid values are `hidden`, `visible`.' end newproperty(:version) do desc 'The version property. Valid values are `1`, `2`, `3`, `4`, `current`.' end newproperty(:volsize) do desc 'The volsize property. Valid values are ``' end newproperty(:vscan) do desc 'The vscan property. Valid values are `on`, `off`.' end newproperty(:xattr) do desc 'The xattr property. Valid values are `on`, `off`.' end newproperty(:zoned) do desc 'The zoned property. Valid values are `on`, `off`.' end autorequire(:zpool) do # strip the zpool off the zfs name and autorequire it [@parameters[:name].value.split('/')[0]] end autorequire(:zfs) do # slice and dice, we want all the zfs before this one names = @parameters[:name].value.split('/') names.slice(1..-2).reduce([]) { |a, v| a << "#{a.last}/#{v}" }.map { |fs| names[0] + fs } end end end diff --git a/spec/unit/provider/zfs/zfs_spec.rb b/spec/unit/provider/zfs/zfs_spec.rb index b41b591..dd7b668 100644 --- a/spec/unit/provider/zfs/zfs_spec.rb +++ b/spec/unit/provider/zfs/zfs_spec.rb @@ -1,165 +1,165 @@ require 'spec_helper' describe Puppet::Type.type(:zfs).provider(:zfs) do let(:name) { 'myzfs' } let(:zfs) { '/usr/sbin/zfs' } let(:resource) do Puppet::Type.type(:zfs).new(name: name, provider: :zfs) end let(:provider) { resource.provider } before(:each) do allow(provider.class).to receive(:which).with('zfs') { zfs } end context '.instances' do it 'has an instances method' do expect(provider.class).to respond_to(:instances) end it 'lists instances' do allow(provider.class).to receive(:zfs).with(:list, '-H') { File.read(my_fixture('zfs-list.out')) } instances = provider.class.instances.map { |p| { name: p.get(:name), ensure: p.get(:ensure) } } expect(instances.size).to eq(2) expect(instances[0]).to eq(name: 'rpool', ensure: :present) expect(instances[1]).to eq(name: 'rpool/ROOT', ensure: :present) end end context '#add_properties' do it 'returns an array of properties' do resource[:mountpoint] = '/foo' expect(provider.add_properties).to eq(['-o', 'mountpoint=/foo']) end it 'returns an empty array' do expect(provider.add_properties).to eq([]) end end context '#create' do it 'executes zfs create' do expect(provider).to receive(:zfs).with(:create, name) provider.create end Puppet::Type.type(:zfs).validproperties.each do |prop| next if [:ensure, :volsize].include?(prop) it "should include property #{prop}" do resource[prop] = prop expect(provider).to receive(:zfs).with(:create, '-o', "#{prop}=#{prop}", name) provider.create end end it 'uses -V for the volsize property' do resource[:volsize] = '10' expect(provider).to receive(:zfs).with(:create, '-V', '10', name) provider.create end end context '#destroy' do it 'executes zfs destroy' do expect(provider).to receive(:zfs).with(:destroy, name) provider.destroy end end context '#exists?' do it 'returns true if the resource exists' do # return stuff because we have to slice and dice it expect(provider).to receive(:zfs).with(:list, name) expect(provider).to be_exists end it "returns false if returned values don't match the name" do expect(provider).to receive(:zfs).with(:list, name).and_raise(Puppet::ExecutionFailure, 'Failed') expect(provider).not_to be_exists end end describe 'zfs properties' do [:aclinherit, :aclmode, :atime, :canmount, :checksum, :compression, :copies, :dedup, :devices, :exec, :logbias, - :mountpoint, :nbmand, :primarycache, :quota, :readonly, + :mountpoint, :nbmand, :overlay, :primarycache, :quota, :readonly, :recordsize, :refquota, :refreservation, :reservation, :secondarycache, :setuid, :shareiscsi, :sharenfs, :sharesmb, :snapdir, :version, :volsize, :vscan, :xattr].each do |prop| it "should get #{prop}" do expect(provider).to receive(:zfs).with(:get, '-H', '-o', 'value', prop, name).and_return("value\n") expect(provider.send(prop)).to eq('value') end it "should set #{prop}=value" do expect(provider).to receive(:zfs).with(:set, "#{prop}=value", name) provider.send("#{prop}=", 'value') end end end describe 'zoned' do context 'on FreeBSD' do before(:each) do allow(Facter).to receive(:value).with(:operatingsystem).and_return('FreeBSD') end it "gets 'jailed' property" do expect(provider).to receive(:zfs).with(:get, '-H', '-o', 'value', :jailed, name).and_return("value\n") expect(provider.send('zoned')).to eq('value') end it 'sets jalied=value' do expect(provider).to receive(:zfs).with(:set, 'jailed=value', name) provider.send('zoned=', 'value') end end context 'when not running FreeBSD' do before(:each) do allow(Facter).to receive(:value).with(:operatingsystem).and_return('Solaris') end it "gets 'zoned' property" do expect(provider).to receive(:zfs).with(:get, '-H', '-o', 'value', :zoned, name).and_return("value\n") expect(provider.send('zoned')).to eq('value') end it 'sets zoned=value' do expect(provider).to receive(:zfs).with(:set, 'zoned=value', name) provider.send('zoned=', 'value') end end end describe 'acltype' do context 'when available' do it "gets 'acltype' property" do expect(provider).to receive(:zfs).with(:get, '-H', '-o', 'value', :acltype, name).and_return("value\n") expect(provider.send('acltype')).to eq('value') end it 'sets acltype=value' do expect(provider).to receive(:zfs).with(:set, 'acltype=value', name) provider.send('acltype=', 'value') end end context 'when not available' do it "gets '-' for the acltype property" do expect(provider).to receive(:zfs).with(:get, '-H', '-o', 'value', :acltype, name).and_raise(RuntimeError, 'not valid') expect(provider.send('acltype')).to eq('-') end it 'does not error out when trying to set acltype' do expect(provider).to receive(:zfs).with(:set, 'acltype=value', name).and_raise(RuntimeError, 'not valid') expect { provider.send('acltype=', 'value') }.not_to raise_error end end end end diff --git a/spec/unit/type/zfs_spec.rb b/spec/unit/type/zfs_spec.rb index 2310593..a595302 100644 --- a/spec/unit/type/zfs_spec.rb +++ b/spec/unit/type/zfs_spec.rb @@ -1,43 +1,43 @@ require 'spec_helper' describe Puppet::Type.type(:zfs) do - properties = [:ensure, :mountpoint, :compression, :copies, :quota, :reservation, :sharenfs, :snapdir] + properties = [:ensure, :mountpoint, :compression, :copies, :overlay, :quota, :reservation, :sharenfs, :snapdir] properties.each do |property| it "should have a #{property} property" do expect(described_class.attrclass(property).ancestors).to be_include(Puppet::Property) end end parameters = [:name] parameters.each do |parameter| it "should have a #{parameter} parameter" do expect(described_class.attrclass(parameter).ancestors).to be_include(Puppet::Parameter) end end it 'autorequires the containing zfs and the zpool' do zfs_provider = instance_double 'provider' allow(zfs_provider).to receive(:name).and_return(:zfs) allow(described_class).to receive(:defaultprovider) { zfs_provider } zpool_provider = instance_double 'provider' allow(zpool_provider).to receive(:name).and_return(:zpool) allow(Puppet::Type.type(:zpool)).to receive(:defaultprovider) { zpool_provider } foo_pool = Puppet::Type.type(:zpool).new(name: 'foo') foo_bar_zfs = described_class.new(name: 'foo/bar') foo_bar_baz_zfs = described_class.new(name: 'foo/bar/baz') foo_bar_baz_buz_zfs = described_class.new(name: 'foo/bar/baz/buz') Puppet::Resource::Catalog.new :testing do |conf| [foo_pool, foo_bar_zfs, foo_bar_baz_zfs, foo_bar_baz_buz_zfs].each { |resource| conf.add_resource resource } end req = foo_bar_baz_buz_zfs.autorequire.map { |edge| edge.source.ref } [foo_pool.ref, foo_bar_zfs.ref, foo_bar_baz_zfs.ref].each { |ref| expect(req.include?(ref)).to eq(true) } end end