diff --git a/lib/puppet/provider/zpool/zpool.rb b/lib/puppet/provider/zpool/zpool.rb index ce40c0a..4c17e73 100644 --- a/lib/puppet/provider/zpool/zpool.rb +++ b/lib/puppet/provider/zpool/zpool.rb @@ -1,193 +1,195 @@ 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 get_zpool_property(prop) zpool(:get, prop, @resource[:name]).split("\n").reverse.map { |line| name, _property, value, _source = line.split("\s") value if name == @resource[:name] }.shift 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 'cache' + sym = :cache when %r{^mirror|^raidz1|^raidz2} sym = (value =~ %r{^mirror}) ? :mirror : :raidz pool[:raid_parity] = 'raidz2' if value =~ %r{^raidz2} else # 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 add_pool_properties properties = [] [:ashift, :autoexpand, :failmode].each do |property| if (value = @resource[property]) && value != '' properties << '-o' << "#{property}=#{value}" end end properties end def create - zpool(*([:create] + add_pool_properties + [@resource[:pool]] + build_vdevs + build_named('spare') + build_named('log'))) + zpool(*([:create] + add_pool_properties + [@resource[:pool]] + build_vdevs + build_named('spare') + build_named('log') + build_named('cache'))) 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| + [:disk, :mirror, :raidz, :log, :spare, :cache].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 [:autoexpand, :failmode].each do |field| define_method(field) do get_zpool_property(field) end define_method(field.to_s + '=') do |should| zpool(:set, "#{field}=#{should}", @resource[:name]) end end # Borrow the code from the ZFS provider here so that we catch and return '-' # as ashift is linux only. # see lib/puppet/provider/zfs/zfs.rb PARAMETER_UNSET_OR_NOT_AVAILABLE = '-'.freeze define_method(:ashift) do begin get_zpool_property(:ashift) rescue PARAMETER_UNSET_OR_NOT_AVAILABLE end end define_method('ashift=') do |should| begin zpool(:set, "ashift=#{should}", @resource[:name]) rescue PARAMETER_UNSET_OR_NOT_AVAILABLE end end end diff --git a/lib/puppet/type/zpool.rb b/lib/puppet/type/zpool.rb index d1b4a8d..2047e9f 100644 --- a/lib/puppet/type/zpool.rb +++ b/lib/puppet/type/zpool.rb @@ -1,123 +1,127 @@ # ZPool type module Puppet # Puppet::Property class Property # VDev class class VDev < Property # @param array the array to be flattened and sorted # @return [Array] returns a flattened and sorted array def flatten_and_sort(array) array = [array] unless array.is_a? Array array.map { |a| a.split(' ') }.flatten.sort end # @param is the current state of the object # @return [Boolean] if the resource is in sync with what it should be def insync?(is) return @should == [:absent] if is == :absent flatten_and_sort(is) == flatten_and_sort(@should) end end # MultiVDev class class MultiVDev < VDev # @param is the current state of the object # @return [Boolean] if the resource is in sync with what it should be def insync?(is) return @should == [:absent] if is == :absent return false unless is.length == @should.length is.each_with_index { |list, i| return false unless flatten_and_sort(list) == flatten_and_sort(@should[i]) } # if we made it this far we are in sync true end end end Type.newtype(:zpool) do desc <<-DESC Manage zpools. Create and delete zpools. The provider WILL NOT SYNC, only report differences. - Supports vdevs with mirrors, raidz, logs and spares. + Supports vdevs with mirrors, raidz, logs, spares, and cache. @example Using zpool. zpool { 'tstpool': ensure => present, disk => '/ztstpool/dsk', } DESC ensurable newproperty(:disk, array_matching: :all, parent: Puppet::Property::VDev) do desc 'The disk(s) for this pool. Can be an array or a space separated string.' end newproperty(:mirror, array_matching: :all, parent: Puppet::Property::MultiVDev) 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| raise ArgumentError, _('mirror names must be provided as string separated, not a comma-separated list') if value.include?(',') end end newproperty(:raidz, array_matching: :all, parent: Puppet::Property::MultiVDev) 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| raise ArgumentError, _('raid names must be provided as string separated, not a comma-separated list') if value.include?(',') end end newproperty(:spare, array_matching: :all, parent: Puppet::Property::VDev) do desc 'Spare disk(s) for this pool.' end newproperty(:log, array_matching: :all, parent: Puppet::Property::VDev) do desc 'Log disks for this pool. This type does not currently support mirroring of log disks.' end + newproperty(:cache, array_matching: :all, parent: Puppet::Property::VDev) do + desc 'Cache disks for this pool.' + end + newproperty(:ashift) do desc 'The Alignment Shift for the vdevs in the given pool.' validate do |_value| raise Puppet::Error _('This property is only supported on Linux') unless Facter.value(:kernel) == 'Linux' end end newproperty(:autoexpand) do desc 'The autoexpand setting for the given pool. Valid values are `on` or `off`' end newproperty(:failmode) do desc 'The failmode setting for the given pool. Valid values are `wait`, `continue` or `panic`' end newparam(:pool) do desc 'The name for this pool.' isnamevar end newparam(:raid_parity) do desc 'Determines parity when using the `raidz` parameter.' end validate do has_should = [:disk, :mirror, :raidz].select { |prop| should(prop) } raise _('You cannot specify %{multiple_props} on this type (only one)') % { multiple_props: has_should.join(' and ') } if has_should.length > 1 end end end diff --git a/spec/unit/provider/zpool/zpool_spec.rb b/spec/unit/provider/zpool/zpool_spec.rb index bf17159..416d989 100644 --- a/spec/unit/provider/zpool/zpool_spec.rb +++ b/spec/unit/provider/zpool/zpool_spec.rb @@ -1,314 +1,329 @@ 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']) + zpool_data.concat ['logs', 'log_disk', 'log_disk2'] + expect(provider.process_zpool_data(zpool_data)[:log]).to eq(['log_disk log_disk2']) + end + end + + describe 'when there is a cache' do + it 'adds the cache disk to the hash' do + zpool_data.concat ['cache', 'cache_disk'] + expect(provider.process_zpool_data(zpool_data)[:cache]).to eq(['cache_disk']) + end + end + + describe 'when there are two caches' do + it 'adds the cache disks to the hash as a single string' do + zpool_data.concat ['cache', 'cache_disk', 'cache_disk2'] + expect(provider.process_zpool_data(zpool_data)[:cache]).to eq(['cache_disk cache_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 for configurable options' do [:autoexpand, :failmode].each do |field| it "should get the #{field} value from the pool" do allow(provider).to receive(:zpool).with(:get, field, name).and_return("NAME PROPERTY VALUE SOURCE\n#{name} #{field} value local") expect(provider.send(field)).to eq('value') end it "should set #{field}=value" do expect(provider).to receive(:zpool).with(:set, "#{field}=value", name) provider.send("#{field}=", 'value') end end end describe 'when calling the getters and setters for ashift' do context 'when available' do it "gets 'ashift' property" do expect(provider).to receive(:zpool).with(:get, :ashift, name).and_return("NAME PROPERTY VALUE SOURCE\n#{name} ashift value local") expect(provider.send('ashift')).to eq('value') end it 'sets ashift=value' do expect(provider).to receive(:zpool).with(:set, 'ashift=value', name) provider.send('ashift=', 'value') end end context 'when not available' do it "gets '-' for the ashift property" do expect(provider).to receive(:zpool).with(:get, :ashift, name).and_raise(RuntimeError, 'not valid') expect(provider.send('ashift')).to eq('-') end it 'does not error out when trying to set ashift' do expect(provider).to receive(:zpool).with(:set, 'ashift=value', name).and_raise(RuntimeError, 'not valid') expect { provider.send('ashift=', 'value') }.not_to raise_error end end end describe 'when calling the getters and setters' do - [:disk, :mirror, :raidz, :log, :spare].each do |field| + [:disk, :mirror, :raidz, :log, :spare, :cache].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') + resource[:cache] = ['value3'] + expect(provider).to receive(:zpool).with(:create, name, 'disk1', 'spare', 'value1', 'log', 'value2', 'cache', 'value3') 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 describe 'when creating a zpool with options' do before(:each) do resource[:disk] = 'disk1' end [:ashift, :autoexpand, :failmode].each do |field| it "should include field #{field}" do resource[field] = field expect(provider).to receive(:zpool).with(:create, '-o', "#{field}=#{field}", name, 'disk1') provider.create end 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 diff --git a/spec/unit/type/zpool_spec.rb b/spec/unit/type/zpool_spec.rb index 3ddbc2b..d63f8d7 100644 --- a/spec/unit/type/zpool_spec.rb +++ b/spec/unit/type/zpool_spec.rb @@ -1,107 +1,107 @@ require 'spec_helper' describe 'zpool' do describe Puppet::Type.type(:zpool) do - properties = [:ensure, :disk, :mirror, :raidz, :spare, :log, :autoexpand, :failmode, :ashift] + properties = [:ensure, :disk, :mirror, :raidz, :spare, :log, :autoexpand, :failmode, :ashift, :cache] 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 = [:pool, :raid_parity] parameters.each do |parameter| it "should have a #{parameter} parameter" do expect(described_class.attrclass(parameter).ancestors).to be_include(Puppet::Parameter) end end end describe Puppet::Property::VDev do let(:resource) { instance_double('resource', :[]= => nil, :property => nil) } let(:vdev) do described_class.new( resource: resource, ) end before(:each) do described_class.initvars end it 'is insync if the devices are the same' do vdev.should = ['dev1 dev2'] expect(vdev).to be_safe_insync(['dev2 dev1']) end it 'is out of sync if the devices are not the same' do vdev.should = ['dev1 dev3'] expect(vdev).not_to be_safe_insync(['dev2 dev1']) end it 'is insync if the devices are the same and the should values are comma separated' do vdev.should = ['dev1', 'dev2'] expect(vdev).to be_safe_insync(['dev2 dev1']) end it 'is out of sync if the device is absent and should has a value' do vdev.should = ['dev1', 'dev2'] expect(vdev).not_to be_safe_insync(:absent) end it 'is insync if the device is absent and should is absent' do vdev.should = [:absent] expect(vdev).to be_safe_insync(:absent) end end describe Puppet::Property::MultiVDev do let(:resource) { instance_double('resource', :[]= => nil, :property => nil) } let(:multi_vdev) do described_class.new( resource: resource, ) end before(:each) do described_class.initvars end it 'is insync if the devices are the same' do multi_vdev.should = ['dev1 dev2'] expect(multi_vdev).to be_safe_insync(['dev2 dev1']) end it 'is out of sync if the devices are not the same' do multi_vdev.should = ['dev1 dev3'] expect(multi_vdev).not_to be_safe_insync(['dev2 dev1']) end it 'is out of sync if the device is absent and should has a value' do multi_vdev.should = ['dev1', 'dev2'] expect(multi_vdev).not_to be_safe_insync(:absent) end it 'is insync if the device is absent and should is absent' do multi_vdev.should = [:absent] expect(multi_vdev).to be_safe_insync(:absent) end describe 'when there are multiple lists of devices' do it 'is in sync if each group has the same devices' do multi_vdev.should = ['dev1 dev2', 'dev3 dev4'] expect(multi_vdev).to be_safe_insync(['dev2 dev1', 'dev3 dev4']) end it 'is out of sync if any group has the different devices' do multi_vdev.should = ['dev1 devX', 'dev3 dev4'] expect(multi_vdev).not_to be_safe_insync(['dev2 dev1', 'dev3 dev4']) end it 'is out of sync if devices are in the wrong group' do multi_vdev.should = ['dev1 dev2', 'dev3 dev4'] expect(multi_vdev).not_to be_safe_insync(['dev2 dev3', 'dev1 dev4']) end end end end