diff --git a/lib/puppet/provider/zpool/zpool.rb b/lib/puppet/provider/zpool/zpool.rb index c095c6a..ce40c0a 100644 --- a/lib/puppet/provider/zpool/zpool.rb +++ b/lib/puppet/provider/zpool/zpool.rb @@ -1,144 +1,193 @@ 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 %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, @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'))) 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 + + [: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 5c479b8..d1b4a8d 100644 --- a/lib/puppet/type/zpool.rb +++ b/lib/puppet/type/zpool.rb @@ -1,107 +1,123 @@ # 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. @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(: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 cfbd2c5..bf17159 100644 --- a/spec/unit/provider/zpool/zpool_spec.rb +++ b/spec/unit/provider/zpool/zpool_spec.rb @@ -1,265 +1,314 @@ 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 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| 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 + + 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 97eda6f..3ddbc2b 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] + properties = [:ensure, :disk, :mirror, :raidz, :spare, :log, :autoexpand, :failmode, :ashift] 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