diff --git a/lib/puppet/provider/zpool/zpool.rb b/lib/puppet/provider/zpool/zpool.rb index 3380530..c095c6a 100644 --- a/lib/puppet/provider/zpool/zpool.rb +++ b/lib/puppet/provider/zpool/zpool.rb @@ -1,139 +1,144 @@ Puppet::Type.type(:zpool).provide(:zpool) do desc 'Provider for zpool.' commands zpool: 'zpool' # NAME SIZE ALLOC FREE CAP HEALTH ALTROOT def self.instances zpool(:list, '-H').split("\n").map do |line| name, _size, _alloc, _free, _cap, _health, _altroot = line.split(%r{\s+}) new(name: name, ensure: :present) end end def process_zpool_data(pool_array) if pool_array == [] return Hash.new(:absent) end # get the name and get rid of it pool = {} pool[:pool] = pool_array[0] pool_array.shift tmp = [] # order matters here :( pool_array.reverse_each do |value| sym = nil case value when 'spares' sym = :spare when 'logs' sym = :log when %r{^mirror|^raidz1|^raidz2} sym = (value =~ %r{^mirror}) ? :mirror : :raidz pool[:raid_parity] = 'raidz2' if value =~ %r{^raidz2} else - tmp << value + # get full drive name if the value is a partition (Linux only) + tmp << if Facter.value(:kernel) == 'Linux' && value =~ %r{/dev/(:?[a-z]+1|disk/by-id/.+-part1)$} + execute("lsblk -p -no pkname #{value}").chomp + else + value + end sym = :disk if value == pool_array.first end if sym pool[sym] = (pool[sym]) ? pool[sym].unshift(tmp.reverse.join(' ')) : [tmp.reverse.join(' ')] tmp.clear end end pool end # rubocop:disable Style/AccessorMethodName # rubocop:disable Style/NumericPredicate def get_pool_data # https://docs.oracle.com/cd/E19082-01/817-2271/gbcve/index.html # we could also use zpool iostat -v mypool for a (little bit) cleaner output zpool_opts = case Facter.value(:kernel) # use full device names ("-P") on Linux/ZOL to prevent # mismatches between creation and display paths: when 'Linux' '-P' else '' end out = execute("zpool status #{zpool_opts} #{@resource[:pool]}", failonfail: false, combine: false) zpool_data = out.lines.select { |line| line.index("\t") == 0 }.map { |l| l.strip.split("\s")[0] } zpool_data.shift zpool_data end def current_pool @current_pool = process_zpool_data(get_pool_data) unless defined?(@current_pool) && @current_pool @current_pool end def flush @current_pool = nil end # Adds log and spare def build_named(name) prop = @resource[name.to_sym] if prop [name] + prop.map { |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.map { |a| [prefix] + a.split(' ') }.flatten end # builds up the vdevs for create command def build_vdevs disk = @resource[:disk] mirror = @resource[:mirror] raidz = @resource[:raidz] if disk disk.map { |d| d.split(' ') }.flatten elsif mirror handle_multi_arrays('mirror', mirror) elsif raidz handle_multi_arrays(raidzarity, raidz) end end def create zpool(*([:create, @resource[:pool]] + build_vdevs + build_named('spare') + build_named('log'))) end def destroy 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 # rubocop:disable Style/SignalException define_method(field.to_s + '=') do |should| fail "zpool #{field} can't be changed. should be #{should}, currently is #{current_pool[field]}" end end end diff --git a/spec/unit/provider/zfs/zfs_spec.rb b/spec/unit/provider/zfs/zfs_spec.rb index dd7b668..266e146 100644 --- a/spec/unit/provider/zfs/zfs_spec.rb +++ b/spec/unit/provider/zfs/zfs_spec.rb @@ -1,165 +1,166 @@ 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 } + allow(Facter).to receive(:value).with(:kernel).and_return('Linux') 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, :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/provider/zpool/zpool_spec.rb b/spec/unit/provider/zpool/zpool_spec.rb index edec453..cfbd2c5 100644 --- a/spec/unit/provider/zpool/zpool_spec.rb +++ b/spec/unit/provider/zpool/zpool_spec.rb @@ -1,255 +1,265 @@ require 'spec_helper' describe Puppet::Type.type(:zpool).provider(:zpool) do let(:name) { 'mypool' } let(:zpool) { '/usr/sbin/zpool' } let(:resource) do Puppet::Type.type(:zpool).new(name: name, provider: :zpool) end let(:provider) { resource.provider } before(:each) do allow(provider.class).to receive(:which).with('zpool') { zpool } + allow(Facter).to receive(:value).with(:kernel).and_return('Linux') end context '#current_pool' do it 'calls process_zpool_data with the result of get_pool_data only once' do allow(provider).to receive(:get_pool_data).and_return(['foo', 'disk']) allow(provider).to receive(:process_zpool_data).with(['foo', 'disk']) { 'stuff' } expect(provider).to receive(:process_zpool_data).with(['foo', 'disk']).once provider.current_pool provider.current_pool end end describe 'self.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(:zpool).with(:list, '-H') { File.read(my_fixture('zpool-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: 'mypool', ensure: :present) end end context '#flush' do it 'reloads the pool' do allow(provider).to receive(:get_pool_data) allow(provider).to receive(:process_zpool_data).and_return('stuff') expect(provider).to receive(:process_zpool_data).twice provider.current_pool provider.flush provider.current_pool end end context '#process_zpool_data' do let(:zpool_data) { ['foo', 'disk'] } describe 'when there is no data' do it 'returns a hash with ensure=>:absent' do expect(provider.process_zpool_data([])[:ensure]).to eq(:absent) end end + describe 'when there are full path disks on Linux' do + it 'munges partitions into disk names' do + allow(provider).to receive(:execute).with('lsblk -p -no pkname /dev/sdc1').and_return('/dev/sdc') + allow(provider).to receive(:execute).with('lsblk -p -no pkname /dev/disk/by-id/disk_serial-0:0-part1').and_return('/dev/disk/by-id/disk_serial-0:0') + zpool_data = ['foo', '/dev/sdc1', '/dev/disk/by-id/disk_serial-0:0-part1'] + expect(provider.process_zpool_data(zpool_data)[:disk]).to eq(['/dev/sdc /dev/disk/by-id/disk_serial-0:0']) + end + end + describe 'when there is a spare' do it 'adds the spare disk to the hash' do zpool_data.concat ['spares', 'spare_disk'] expect(provider.process_zpool_data(zpool_data)[:spare]).to eq(['spare_disk']) end end describe 'when there are two spares' do it 'adds the spare disk to the hash as a single string' do zpool_data.concat ['spares', 'spare_disk', 'spare_disk2'] expect(provider.process_zpool_data(zpool_data)[:spare]).to eq(['spare_disk spare_disk2']) end end describe 'when there is a log' do it 'adds the log disk to the hash' do zpool_data.concat ['logs', 'log_disk'] expect(provider.process_zpool_data(zpool_data)[:log]).to eq(['log_disk']) end end describe 'when there are two logs' do it 'adds the log disks to the hash as a single string' do zpool_data.concat ['spares', 'spare_disk', 'spare_disk2'] expect(provider.process_zpool_data(zpool_data)[:spare]).to eq(['spare_disk spare_disk2']) end end describe 'when the vdev is a single mirror' do it 'calls create_multi_array with mirror' do zpool_data = ['mirrorpool', 'mirror', 'disk1', 'disk2'] expect(provider.process_zpool_data(zpool_data)[:mirror]).to eq(['disk1 disk2']) end end describe 'when the vdev is a single mirror on solaris 10u9 or later' do it 'calls create_multi_array with mirror' do zpool_data = ['mirrorpool', 'mirror-0', 'disk1', 'disk2'] expect(provider.process_zpool_data(zpool_data)[:mirror]).to eq(['disk1 disk2']) end end describe 'when the vdev is a double mirror' do it 'calls create_multi_array with mirror' do zpool_data = ['mirrorpool', 'mirror', 'disk1', 'disk2', 'mirror', 'disk3', 'disk4'] expect(provider.process_zpool_data(zpool_data)[:mirror]).to eq(['disk1 disk2', 'disk3 disk4']) end end describe 'when the vdev is a double mirror on solaris 10u9 or later' do it 'calls create_multi_array with mirror' do zpool_data = ['mirrorpool', 'mirror-0', 'disk1', 'disk2', 'mirror-1', 'disk3', 'disk4'] expect(provider.process_zpool_data(zpool_data)[:mirror]).to eq(['disk1 disk2', 'disk3 disk4']) end end describe 'when the vdev is a raidz1' do it 'calls create_multi_array with raidz1' do zpool_data = ['mirrorpool', 'raidz1', 'disk1', 'disk2'] expect(provider.process_zpool_data(zpool_data)[:raidz]).to eq(['disk1 disk2']) end end describe 'when the vdev is a raidz1 on solaris 10u9 or later' do it 'calls create_multi_array with raidz1' do zpool_data = ['mirrorpool', 'raidz1-0', 'disk1', 'disk2'] expect(provider.process_zpool_data(zpool_data)[:raidz]).to eq(['disk1 disk2']) end end describe 'when the vdev is a raidz2' do it 'calls create_multi_array with raidz2 and set the raid_parity' do zpool_data = ['mirrorpool', 'raidz2', 'disk1', 'disk2'] pool = provider.process_zpool_data(zpool_data) expect(pool[:raidz]).to eq(['disk1 disk2']) expect(pool[:raid_parity]).to eq('raidz2') end end describe 'when the vdev is a raidz2 on solaris 10u9 or later' do it 'calls create_multi_array with raidz2 and set the raid_parity' do zpool_data = ['mirrorpool', 'raidz2-0', 'disk1', 'disk2'] pool = provider.process_zpool_data(zpool_data) expect(pool[:raidz]).to eq(['disk1 disk2']) expect(pool[:raid_parity]).to eq('raidz2') end end end describe 'when calling the getters and setters' do [:disk, :mirror, :raidz, :log, :spare].each do |field| describe "when calling #{field}" do it "should get the #{field} value from the current_pool hash" do pool_hash = {} pool_hash[field] = 'value' allow(provider).to receive(:current_pool) { pool_hash } expect(provider.send(field)).to eq('value') end end describe "when setting the #{field}" do it "should fail if readonly #{field} values change" do allow(provider).to receive(:current_pool) { Hash.new('currentvalue') } expect { provider.send((field.to_s + '=').to_sym, 'shouldvalue') }.to raise_error(Puppet::Error, %r{can\'t be changed}) end end end end context '#create' do context 'when creating disks for a zpool' do before(:each) do resource[:disk] = 'disk1' end it 'calls create with the build_vdevs value' do expect(provider).to receive(:zpool).with(:create, name, 'disk1') provider.create end it "calls create with the 'spares' and 'log' values" do resource[:spare] = ['value1'] resource[:log] = ['value2'] expect(provider).to receive(:zpool).with(:create, name, 'disk1', 'spare', 'value1', 'log', 'value2') provider.create end end context 'when creating mirrors for a zpool' do it "executes 'create' for a single group of mirrored devices" do resource[:mirror] = ['disk1 disk2'] expect(provider).to receive(:zpool).with(:create, name, 'mirror', 'disk1', 'disk2') provider.create end it "repeats the 'mirror' keyword between groups of mirrored devices" do resource[:mirror] = ['disk1 disk2', 'disk3 disk4'] expect(provider).to receive(:zpool).with(:create, name, 'mirror', 'disk1', 'disk2', 'mirror', 'disk3', 'disk4') provider.create end end describe 'when creating raidz for a zpool' do it "executes 'create' for a single raidz group" do resource[:raidz] = ['disk1 disk2'] expect(provider).to receive(:zpool).with(:create, name, 'raidz1', 'disk1', 'disk2') provider.create end it "execute 'create' for a single raidz2 group" do resource[:raidz] = ['disk1 disk2'] resource[:raid_parity] = 'raidz2' expect(provider).to receive(:zpool).with(:create, name, 'raidz2', 'disk1', 'disk2') provider.create end it "repeats the 'raidz1' keyword between each group of raidz devices" do resource[:raidz] = ['disk1 disk2', 'disk3 disk4'] expect(provider).to receive(:zpool).with(:create, name, 'raidz1', 'disk1', 'disk2', 'raidz1', 'disk3', 'disk4') provider.create end end end context '#delete' do it 'calls zpool with destroy and the pool name' do expect(provider).to receive(:zpool).with(:destroy, name) provider.destroy end end context '#exists?' do it 'gets the current pool' do allow(provider).to receive(:current_pool).and_return(pool: 'somepool') expect(provider).to receive(:current_pool) provider.exists? end it 'returns false if the current_pool is absent' do allow(provider).to receive(:current_pool).and_return(pool: :absent) expect(provider).to receive(:current_pool) expect(provider).not_to be_exists end it 'returns true if the current_pool has values' do allow(provider).to receive(:current_pool).and_return(pool: name) expect(provider).to receive(:current_pool) expect(provider).to be_exists end end end