cookbooks/fb_storage/spec/storage_spec.rb (2,551 lines of code) (raw):

# vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2 # # Copyright (c) 2016-present, Facebook, Inc. # All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. require './spec/spec_helper' require_relative '../../fb_fstab/libraries/default' require_relative '../../fb_helpers/libraries/node_methods' require_relative '../libraries/storage' describe FB::Storage do let(:node) { Chef::Node.new } let(:attr_name) do if node['filesystem2'] 'filesystem2' else 'filesystem' end end before do node.automatic['fb'] = {} end context '#eligble_devices' do before do { '/' => '/dev/sda', '/boot' => '/dev/sdb', }.each do |mp, device| allow(node).to receive(:device_of_mount).with(mp).and_return(device) allow(File).to receive(:realpath).with(device).and_return(device) if mp == '/boot' boot_mock = double allow(Pathname).to receive(:new).with(mp).and_return(boot_mock) allow(boot_mock).to receive(:mountpoint?).and_return(device) end end end it 'should not include root nor boot drives' do node.automatic['block_device'] = { 'sda' => {}, 'sdb' => {}, 'sdc' => {}, } expect(FB::Storage.eligible_devices(node)).to eq(['sdc']) end it 'should skip ram, loop, and md devices' do node.automatic['block_device'] = { 'sda' => {}, 'fioa' => {}, 'loop0' => {}, 'ram0' => {}, 'md0' => {}, 'dm-0' => {}, 'sr0' => {}, } expect(FB::Storage.eligible_devices(node)).to eq(['fioa']) end # This isn't actually handled yet, so comment it out until we fix the code # it 'should skip when we cannot find root\'s device' do # node.automatic['block_device'] = { # 'sda' => {}, # 'sdb' => {}, # 'sdc' => {}, # } # allow(node).to receive(:device_of_mount).with('/').and_return(nil) # expect(FB::Storage.eligible_devices(node)).to eq([]) # end end context '#root_device_name' do it 'should work when root is a partition' do { 'sda1' => 'sda', 'nvme0n1p1' => 'nvme0n1', }.each do |partition, device| node.automatic['block_device'][device] = {} allow(node).to receive(:device_of_mount).with('/').and_return( "/dev/#{partition}", ) allow(File).to receive(:symlink?).with("/dev/#{partition}"). and_return(false) expect(FB::Storage.root_device_name(node)).to eq(device) end end it 'should work when root is a bare device' do %w{ sda md0 }.each do |device| node.automatic['block_device'][device] = {} allow(node).to receive(:device_of_mount).with('/').and_return( "/dev/#{device}", ) allow(File).to receive(:symlink?).with("/dev/#{device}"). and_return(false) expect(FB::Storage.root_device_name(node)).to eq(device) end end it 'should resolve dev mapper to the underlying device' do # '/dev/mapper/transient' is a symlink to /dev/dm-0 mapper_dev = '/dev/mapper/transient' root_dev = 'dm-0' node.automatic['block_device'][root_dev] = {} allow(node).to receive(:device_of_mount).with('/').and_return( mapper_dev, ) allow(File).to receive(:symlink?).with(mapper_dev).and_return(true) allow(File).to receive(:realpath).with(mapper_dev).and_return(root_dev) expect(FB::Storage.root_device_name(node)).to eq(root_dev) end it 'should skip resolving links when /dev is not visible' do # some build environments, like antlir, don't expose /dev # / is on /dev/loop0, but /dev/loop0 doesn't exist root_dev = 'loop0' node.automatic['block_device'][root_dev] = {} allow(node).to receive(:device_of_mount).with('/').and_return( root_dev, ) allow(File).to receive(:symlink?).with(root_dev).and_return(false) expect(FB::Storage.root_device_name(node)).to eq(root_dev) end end context '#partition_device_name' do it 'should insert a p when the device name ends in a number' do FB::Storage.partition_device_name('abcd0', 2). should eq('abcd0p2') end it 'should not insert a p when the device name ends in a number' do FB::Storage.partition_device_name('abcd', 2). should eq('abcd2') end end context '#device_name_from_partition' do it 'should handle devices that end in digits' do { '/dev/md0p1' => '/dev/md0', '/dev/nvme0n1p0' => '/dev/nvme0n1', '/dev/nvme1n2p1' => '/dev/nvme1n2', '/dev/something0p0' => '/dev/something0', }.each do |part, dev| expect(FB::Storage.device_name_from_partition(part)). to eq(dev) end end # see comment in the code for why this is necessary it 'should handle devices that end in digits, even when handed' + ' non-partitions' do { '/dev/md0' => '/dev/md0', '/dev/nvme0n1' => '/dev/nvme0n1', }.each do |part, dev| expect(FB::Storage.device_name_from_partition(part)). to eq(dev) end end it 'should handle devices that do not end in digits' do { '/dev/sda1' => '/dev/sda', '/dev/sdp1' => '/dev/sdp', '/dev/hdb4' => '/dev/hdb', '/dev/sdab9' => '/dev/sdab', '/dev/fioa2' => '/dev/fioa', }.each do |part, dev| expect(FB::Storage.device_name_from_partition(part)). to eq(dev) end end end context '#block_device_split' do it 'splits correctly for valid inputs' do { 'sda' => ['sd', 'a'], 'sdc' => ['sd', 'c'], 'sdaa' => ['sd', 'aa'], 'sdac' => ['sd', 'ac'], 'fioa' => ['fio', 'a'], 'fioc' => ['fio', 'c'], 'nvme0n1' => ['nvme', '0n1'], 'nvme1n4' => ['nvme', '1n4'], 'nvme10n24' => ['nvme', '10n24'], 'nbd3' => ['nbd', '3'], 'nbd12' => ['nbd', '12'], }.each do |input, output| expect(FB::Storage.block_device_split(input)).to eq(output) end end it 'throws errors on invalid inputs' do [ 'hda', 'hdaa', 'fda', 'hda1', 'hda3', # valid devices with a partition, which is invalid 'sda2', 'fioa1', 'nvme0n1p3', 'nbd2p2', ].each do |badinput| expect do FB::Storage.block_device_split(badinput) end.to raise_error( RuntimeError, /fb_storage: Cannot parse #{badinput} for sorting/, ) end end end context '#block_device_sort' do # normal sorting it 'sorts sdb before sdc' do expect(FB::Storage.block_device_sort('sdb', 'sdc', {})). to eq(-1) end # b < aa it 'sorts sdb before sdaa' do expect(FB::Storage.block_device_sort('sdb', 'sdaa', {})). to eq(-1) end it 'does not sort fioa between sdz and sdaa' do expect( ['sdz', 'fioa', 'sdaa'].sort do |a, b| FB::Storage.block_device_sort(a, b, {}) end, ).to eq(['sdz', 'sdaa', 'fioa']) end it 'sorts nvme by card number then namespace number' do expect( [ 'nvme2n2', 'nvme1n2', 'nvme2n4', 'nvme22n1', 'nvme0n22', 'nvme0n2' ].sort do |a, b| FB::Storage.block_device_sort(a, b, {}) end, ).to eq( ['nvme0n2', 'nvme0n22', 'nvme1n2', 'nvme2n2', 'nvme2n4', 'nvme22n1'], ) end it 'sorts prefixes together' do # prefixes by length: sd, fio, nvme expect( [ 'sdac', 'sdq', 'fiob', 'nvme1n1', 'sdb', 'fioa', 'sdaa', 'nvme0n1' ].sort do |a, b| FB::Storage.block_device_sort(a, b, {}) end, ).to eq( ['sdb', 'sdq', 'sdaa', 'sdac', 'fioa', 'fiob', 'nvme0n1', 'nvme1n1'], ) end end context '#hybrid_xfs_md_part_size' do num_sectors_256gb = 500118192 num_sectors_1tb = 1758174768 it 'should return size divided by num_fses with sectors_reserved=0' do allow(FB::Storage).to receive( :hybrid_md_idx_size, ).and_return(num_sectors_256gb) size = FB::Storage.hybrid_xfs_md_part_size(node, 0, 36) expect(size).to eq(6783) # in MiB end it 'should subtract sectors_reserved when provided and then divide' do allow(FB::Storage).to receive( :hybrid_md_idx_size, ).and_return(num_sectors_1tb) size = FB::Storage.hybrid_xfs_md_part_size( node, 0, 36, num_sectors_256gb ) expect(size).to eq(17063) # in MiB end end context '#sort_scsi_slots' do it 'should sort, by column, numerically' do expect( [ '0:2:3:0', '1:0:0:91', '5:01:00:1', '0:12:3:0', '5:10:00:9', '5:9:9:19', ].sort do |a, b| FB::Storage.sort_scsi_slots(a, b) end, ).to eq( [ '0:2:3:0', '0:12:3:0', '1:0:0:91', '5:01:00:1', '5:9:9:19', '5:10:00:9', ], ) end end context '#scsi_device_sort' do let(:mapping) do { '/dev/sdc' => '0:2:3:0', '/dev/sdq' => '1:0:0:91', } end it 'should sort SCSI before non-SCSI' do expect( FB::Storage.scsi_device_sort( '/dev/sdb', '/dev/sdc', mapping, ), ).to eq(1) end it 'should sort non-SCSI after SCSI' do expect( FB::Storage.scsi_device_sort( '/dev/sdc', '/dev/sdb', mapping, ), ).to eq(-1) end it 'should treat two non-SCSI as equal' do expect( FB::Storage.scsi_device_sort( '/dev/sdx', '/dev/sdz', mapping, ), ).to eq(0) end end context '#sort_disk_shelves' do it 'should sort shelves by shelf number then disk number' do expect( [ { 'shelf' => '/dev/bsg/6:0:4:0', 'disk' => 0, }, { 'shelf' => '/dev/bsg/6:0:31:0', 'disk' => 0, }, { 'shelf' => '/dev/bsg/6:1:0:0', 'disk' => 3, }, { 'shelf' => '/dev/bsg/6:1:0:0', 'disk' => 9, }, { 'shelf' => '/dev/bsg/6:0:31:0', 'disk' => 2, }, { 'shelf' => '/dev/bsg/6:0:4:0', 'disk' => 1, }, ].sort do |a, b| FB::Storage.sort_disk_shelves(a, b) end, ).to eq([ # in particular we want to make sure 4 < 31 { 'shelf' => '/dev/bsg/6:0:4:0', 'disk' => 0, }, { 'shelf' => '/dev/bsg/6:0:4:0', 'disk' => 1, }, { 'shelf' => '/dev/bsg/6:0:31:0', 'disk' => 0, }, { 'shelf' => '/dev/bsg/6:0:31:0', 'disk' => 2, }, { 'shelf' => '/dev/bsg/6:1:0:0', 'disk' => 3, }, { 'shelf' => '/dev/bsg/6:1:0:0', 'disk' => 9, }, ]) end end context '#load_previous_disk_order' do it 'reads v1 files correctly' do allow(File).to receive(:size?).with( FB::Storage::PREVIOUS_DISK_ORDER, ).and_return(100) allow(File).to receive(:read).with( FB::Storage::PREVIOUS_DISK_ORDER, ).and_return( '["sdl","sdr","sdy","sdb","sdc","sdt"]', ) ret = FB::Storage.load_previous_disk_order expect(ret).to eq(['sdl', 'sdr', 'sdy', 'sdb', 'sdc', 'sdt']) end end context '#load_previous_disk_order' do it 'reads v2 files correctly' do mapping = { 'xxx' => 'sdl', 'aaa' => 'sdr', 'bbb' => 'sdy', 'ccc' => 'sdb', 'ddd' => 'sdc', 'eee' => 'sdt', } allow(File).to receive(:size?).with( FB::Storage::PREVIOUS_DISK_ORDER, ).and_return(100) allow(File).to receive(:read).with( FB::Storage::PREVIOUS_DISK_ORDER, ).and_return( '{"version":2,"disks":["xxx","aaa","bbb","ccc","ddd","eee"]}', ) mapping.each do |id, dev| sys = "#{FB::Storage::DEV_ID_DIR}/#{id}" allow(File).to receive(:exist?).with(sys).and_return(dev) allow(File).to receive(:readlink).with(sys).and_return("/dev/#{dev}") end ret = FB::Storage.load_previous_disk_order expect(ret).to eq(['sdl', 'sdr', 'sdy', 'sdb', 'sdc', 'sdt']) end it 'does not accept other formats' do allow(File).to receive(:size?).with( FB::Storage::PREVIOUS_DISK_ORDER, ).and_return(100) allow(File).to receive(:read).with( FB::Storage::PREVIOUS_DISK_ORDER, ).and_return( '{"version":3,"disks":{}}', ) expect do FB::Storage.load_previous_disk_order end.to raise_error( RuntimeError, 'fb_storage: Unknown format of persistent-order cache file!', ) end end context '#gen_persistent_disk_data' do it 'writes v2 format' do mapping = { 'xxx' => 'sdl', 'aaa' => 'sdr', 'bbb' => 'sdy', 'ccc' => 'sdb', 'ddd' => 'sdc', 'eee' => 'sdt', } allow(Dir).to receive(:open).with( FB::Storage::DEV_ID_DIR, ).and_return(mapping.keys) mapping.each do |k, v| allow(File).to receive(:readlink).with( "#{FB::Storage::DEV_ID_DIR}/#{k}", ).and_return("/dev/#{v}") end expected = { 'version' => 2, 'disks' => mapping.keys, } ret = FB::Storage.gen_persistent_disk_data(mapping.values) expect(ret).to eq(expected) end end context '#calculate_updated_order' do it 'slots new disks into old slots' do [ # simple example: replacing 'b' with 'f' { 'previous' => ['a', 'b', 'c', 'd', 'e'], 'new' => ['a', 'c', 'd', 'e', 'f'], 'expected' => ['a', 'f', 'c', 'd', 'e'], }, # slightly more complicated # replacing 'b' with 'f' and 'd' with 'g' { 'previous' => ['a', 'b', 'c', 'd', 'e'], 'new' => ['a', 'c', 'e', 'f', 'g'], 'expected' => ['a', 'f', 'c', 'g', 'e'], }, # more complicated example. like the above, but the new ordering # is different from expected, g and f ordered differently (for SCSI slot # or JBOD slot). So... 'b' is now 'g' and 'd' is now 'f' { 'previous' => ['a', 'b', 'c', 'd', 'e'], 'new' => ['a', 'c', 'e', 'g', 'f'], 'expected' => ['a', 'g', 'c', 'f', 'e'], }, # another complicated example. like the above, but the both orderings # are unexpected - 'e' replaced by 'q' and 'c' replaced with 'l' { 'previous' => ['d', 'e', 'a', 'f', 'c'], 'new' => ['d', 'a', 'f', 'q', 'l'], 'expected' => ['d', 'q', 'a', 'f', 'l'], }, # most complicated example... we totally changed our algorithm and # fucked everything up so all the ordering is different, but only one # disk changed. We should ignore the new algorithm and keep things # as they are except for 'c' -> 'l' { 'previous' => ['a', 'b', 'c', 'd', 'e'], 'new' => ['d', 'a', 'e', 'l', 'b'], 'expected' => ['a', 'b', 'l', 'd', 'e'], }, ].each do |example| ret = FB::Storage.calculate_updated_order( example['previous'], example['new'], ) expect(ret).to eq(example['expected']) end end it 'fails when new and old are the same disks' do disks = ['b', 'a', 'd', 'e', 'c'] expect do FB::Storage.calculate_updated_order(disks, disks.sort) end.to raise_error( RuntimeError, /fb_storage: Found no difference between old and new disks/, ) end end context '#sorted_devices' do before do allow(node).to receive(:device_of_mount).and_return('/dev/sda') allow(node).to receive(:virtual?).and_return(false) allow(FB::Storage).to receive(:write_out_disk_order). and_return(nil) allow(FB::Storage).to receive(:devices_to_skip).with(node). and_return(['sda']) end before(:each) do node.default['fb_storage']['_ordered_disks'] = nil end context 'with a cache file' do it 'should use the cache file when all disks are in the same order' do previous_order = ['b', 'e', 'd', 'a', 'c'] expect(FB::Storage).to receive(:load_previous_disk_order). and_return(previous_order) expect(FB::Storage).to receive(:eligible_devices). and_return(previous_order) expect(Chef::Log).to receive(:debug).with(/Using previous/) expect(FB::Storage.sorted_devices(node, [])).to eq( previous_order, ) end it 'should use the cache file when all disks are in a different order' do previous_order = ['b', 'e', 'd', 'a', 'c'] expect(FB::Storage).to receive(:load_previous_disk_order). and_return(previous_order) expect(FB::Storage).to receive(:eligible_devices). and_return(['a', 'b', 'c', 'd', 'e']) expect(Chef::Log).to receive(:debug).with(/Using previous/) expect(FB::Storage.sorted_devices(node, [])).to eq( previous_order, ) end it 'should base the new order on the old order when disks change' do # for can_use_dev_id? node.automatic['block_device'] = {} previous_order = ['sdb', 'sde', 'sdd', 'sdc', 'sdf'] expect(FB::Storage).to receive(:load_previous_disk_order). and_return(previous_order) expect(FB::Storage).to receive(:eligible_devices). and_return(['sdf', 'sdb', 'sdd', 'sde', 'sdg']) expect(Chef::Log).not_to receive(:debug).with(/Using previous/) expect(FB::Storage.sorted_devices(node, [])).to eq( # only sdc was replaced (with sdg) ['sdb', 'sde', 'sdd', 'sdg', 'sdf'], ) end end end context '#_handle_custom_device_order_method' do context 'with a _clowntown_device_order_method' do before(:each) do node.default['fb_storage'][ '_clowntown_device_order_method'] = double('custom_device_order') allow(FB::Storage).to receive( :write_out_disk_order, ).and_return(true) end it 'should call the custom method if firstboot' do allow(node).to receive(:firstboot_tier?).and_return(true) allow(File).to receive(:exist?).with( FB::Storage::FORCE_WRITE_CUSTOM_DISK_ORDER, ).and_return(false) expect( node['fb_storage']['_clowntown_device_order_method'], ).to receive(:call).once FB::Storage._handle_custom_device_order_method(node) end it 'should call the custom method if signal file exists (and delete ' + 'signal file)' do allow(node).to receive(:firstboot_tier?).and_return(false) allow(File).to receive(:exist?).with( FB::Storage::FORCE_WRITE_CUSTOM_DISK_ORDER, ).and_return(true) expect( node['fb_storage']['_clowntown_device_order_method'], ).to receive(:call).once expect(File).to receive(:delete).with( FB::Storage::FORCE_WRITE_CUSTOM_DISK_ORDER, ) FB::Storage._handle_custom_device_order_method(node) end it 'should call the custom method if firstboot and signal file exists' + ' (and delete signal file)' do allow(node).to receive(:firstboot_tier?).and_return(true) allow(File).to receive(:exist?).with( FB::Storage::FORCE_WRITE_CUSTOM_DISK_ORDER, ).and_return(true) expect( node['fb_storage']['_clowntown_device_order_method'], ).to receive(:call).once expect(File).to receive(:delete).with( FB::Storage::FORCE_WRITE_CUSTOM_DISK_ORDER, ) FB::Storage._handle_custom_device_order_method(node) end it 'should not call the custom method if not firstboot or signal file' do allow(node).to receive(:firstboot_tier?).and_return(false) allow(File).to receive(:exist?).with( FB::Storage::FORCE_WRITE_CUSTOM_DISK_ORDER, ).and_return(false) expect( node['fb_storage']['_clowntown_device_order_method'], ).not_to receive(:call) FB::Storage._handle_custom_device_order_method(node) end end context 'without a _clowntown_device_order_method' do before(:each) do node.default['fb_storage'][ '_clowntown_device_order_method'] = nil end it 'should not call the method if firstboot' do allow(node).to receive(:firstboot_tier?).and_return(true) allow(File).to receive(:exist?).with( FB::Storage::FORCE_WRITE_CUSTOM_DISK_ORDER, ).and_return(false) expect( node['fb_storage']['_clowntown_device_order_method'], ).not_to receive(:call) FB::Storage._handle_custom_device_order_method(node) end it 'should not call the method if signal file (and delete file)' do allow(node).to receive(:firstboot_tier?).and_return(false) allow(File).to receive(:exist?).with( FB::Storage::FORCE_WRITE_CUSTOM_DISK_ORDER, ).and_return(true) expect( node['fb_storage']['_clowntown_device_order_method'], ).not_to receive(:call) FB::Storage._handle_custom_device_order_method(node) end it 'should not call the method if neither first boot nor signal file ' + '(and delete the file)' do allow(node).to receive(:firstboot_tier?).and_return(false) allow(File).to receive(:exist?).with( FB::Storage::FORCE_WRITE_CUSTOM_DISK_ORDER, ).and_return(false) expect( node['fb_storage']['_clowntown_device_order_method'], ).not_to receive(:call) FB::Storage._handle_custom_device_order_method(node) end end end context '#build_mapping' do before do allow(node).to receive(:device_of_mount).with('/').and_return('/dev/sda') allow(node).to receive(:device_of_mount).with('/boot'). and_return('/dev/sda') boot_mock = double allow(Pathname).to receive(:new).with('/boot').and_return(boot_mock) allow(boot_mock).to receive(:mountpoint?).and_return('/dev/sda') allow(File).to receive(:realpath).with('/dev/sda').and_return('/dev/sda') allow(node).to receive(:virtual?).and_return(false) allow(FB::Storage).to receive(:load_previous_disk_order). and_return(nil) allow(FB::Storage).to receive(:write_out_disk_order). and_return(nil) end context 'straight disks' do 6.times.each do |i| let("device#{i + 1}".to_sym) do { 'partitions' => [ { 'type' => 'xfs', 'mount_point' => "/data/#{i + 1}", 'opts' => 'default', 'pass' => 2, 'enable_remount' => true, }, ], } end end it 'should build config in consistent order for local storage' do node.default['fb_storage']['devices'] = [device1, device2] node.automatic['block_device'] = { 'sda' => {}, 'sdb' => {}, 'fioa' => {}, } node.automatic['fb'] = {} expected = { :disks => { '/dev/sdb' => device1, '/dev/fioa' => device2, }, :arrays => {}, } expect(FB::Storage.build_mapping(node, [])).to eq(expected) # same thing, but if `block_device` reports a different order node.automatic['block_device'] = { 'fioa' => {}, 'sda' => {}, 'sdb' => {}, } expect(FB::Storage.build_mapping(node, [])).to eq(expected) end it 'should build config in consistent order for shelves' do node.default['fb_storage']['devices'] = [ device1, device2, device3, device4, device5, device6 ] node.automatic['block_device'] = { 'sda' => {}, 'sdb' => {}, 'sdc' => {}, 'sdd' => {}, 'sdl' => {}, 'sdn' => {}, 'sdq' => {}, } node.automatic['fb']['fbjbod']['shelves']['/dev/bsg/6:0:31:0'] = [ '/dev/sdb', '/dev/sdd', '/dev/sdc' ] node.automatic['fb']['fbjbod']['shelves']['/dev/bsg/6:0:15:0'] = [ '/dev/sdq', '/dev/sdl', '/dev/sdn' ] expected = { :disks => { '/dev/sdq' => device1, '/dev/sdl' => device2, '/dev/sdn' => device3, '/dev/sdb' => device4, '/dev/sdd' => device5, '/dev/sdc' => device6, }, :arrays => {}, } expect(FB::Storage.build_mapping(node, [])).to eq(expected) end it 'handles back-compat shelf names' do node.default['fb_storage']['devices'] = [ device1, device2 ] node.automatic['fb']['fbjbod']['shelves']['/dev/sg14'] = [ '/dev/sdb', ] node.automatic['fb']['fbjbod']['shelves']['/dev/sg2'] = [ '/dev/sdq', ] node.automatic['block_device'] = { 'sda' => {}, 'sdb' => {}, 'sdq' => {}, } expected = { :disks => { '/dev/sdq' => device1, '/dev/sdb' => device2, }, :arrays => {}, } expect(FB::Storage.build_mapping(node, [])).to eq(expected) end it 'should build config in consistent order for shelves+flash' do node.default['fb_storage']['devices'] = [ device1, device2, device3, device4 ] node.automatic['block_device'] = { 'sda' => {}, 'sdb' => {}, # fbjbod 'sdc' => {}, # lsicard 'sdd' => {}, # fbjbod 'fioa' => {}, # fio card } node.automatic['fb']['fbjbod']['shelves']['/dev/sg16'] = [ '/dev/sdd', '/dev/sdb' ] expected = { :disks => { # local attached first '/dev/sdc' => device1, '/dev/fioa' => device2, # then shelves, in shelf order '/dev/sdd' => device3, '/dev/sdb' => device4, }, :arrays => {}, } expect(FB::Storage.build_mapping(node, [])).to eq(expected) end it 'should build config in consistent order for scsi slots' do node.default['fb_storage']['devices'] = [ device1, device2, device3 ] node.automatic['block_device'] = { 'sda' => {}, 'sdb' => {}, 'sdc' => {}, 'sdd' => {}, } node.automatic['scsi'] = { '1:2:3:4' => { 'device' => '/dev/sda', }, '0:99:1:2' => { 'device' => '/dev/sdb', }, '9:1:6:5' => { 'device' => '/dev/sdc', }, '0:9:1:2' => { 'device' => '/dev/sdd', }, } expected = { :disks => { # in order of SCSI bus '/dev/sdd' => device1, '/dev/sdb' => device2, '/dev/sdc' => device3, }, :arrays => {}, } expect(FB::Storage.build_mapping(node, [])).to eq(expected) end it 'should build config in consistent order for jbod+scsi+disks' do node.default['fb_storage']['devices'] = [ device1, device2, device3, device4, device5, device6 ] node.automatic['block_device'] = { 'sda' => {}, # root 'sdb' => {}, # jbod 'sdc' => {}, # scsi 'sdd' => {}, # jbod 'fioa' => {}, # flash 'sde' => {}, # drive w/out SCSI addr 'sdf' => {}, # scsi } node.automatic['scsi'] = { '1:2:3:4' => { 'device' => '/dev/sda', }, '9:12:6:5' => { 'device' => '/dev/sdc', }, '9:1:6:5' => { 'device' => '/dev/sdf', }, } node.automatic['fb']['fbjbod']['shelves']['/dev/sg16'] = [ '/dev/sdd', '/dev/sdb' ] expected = { :disks => { # in order of SCSI bus '/dev/sdf' => device1, '/dev/sdc' => device2, # now leftover disk '/dev/sde' => device3, '/dev/fioa' => device4, # now jbod '/dev/sdd' => device5, '/dev/sdb' => device6, }, :arrays => {}, } expect(FB::Storage.build_mapping(node, [])).to eq(expected) end end context 'arrays' do 3.times.each do |array| 5.times.each do |device| let("device#{(array * 5) + device + 1}".to_sym) do { 'partitions' => [ { '_swraid_array' => array, }, ], } end end let("array#{array + 1}".to_sym) do { 'type' => 'xfs', 'mount_point' => "/data/#{array + 1}", 'opts' => 'default', 'pass' => 2, 'enable_remount' => true, 'raid_level' => 5, } end end it 'should build a config with single array mapping' do node.default['fb_storage']['devices'] = [ device1, device2, device3, device4, device5 ] node.default['fb_storage']['arrays'] = [array1] node.automatic['block_device'] = { 'sda' => {}, 'sdb' => {}, 'sdc' => {}, 'sdd' => {}, 'sde' => {}, 'sdf' => {}, } expected_disks = { '/dev/sdb' => device1, '/dev/sdc' => device2, '/dev/sdd' => device3, '/dev/sde' => device4, '/dev/sdf' => device5, } expected_arrays = { '/dev/md0' => array1.merge( { 'members' => expected_disks.keys.map { |x| "#{x}1" } }, ), } expected = { :disks => expected_disks, :arrays => expected_arrays } expect(FB::Storage.build_mapping(node, [])).to eq(expected) end it 'should build a config with multiple array mapping' do node.default['fb_storage']['devices'] = [ device1, device2, device3, device4, device5, device6, device7, device8, device9, device10, device11, device12, device13, device14, device15 ] node.default['fb_storage']['arrays'] = [ array1, array2, array3 ] expected_disks = { '/dev/sdb' => device1, '/dev/sdc' => device2, '/dev/sdd' => device3, '/dev/sde' => device4, '/dev/sdf' => device5, '/dev/sdg' => device6, '/dev/sdh' => device7, '/dev/sdi' => device8, '/dev/sdj' => device9, '/dev/sdk' => device10, '/dev/sdl' => device11, '/dev/sdm' => device12, '/dev/sdn' => device13, '/dev/sdo' => device15, '/dev/sdp' => device15, } expected_disks.each_key do |d| node.automatic['block_device'][File.basename(d)] = {} end node.automatic['block_device']['sda'] = {} expected_arrays = { '/dev/md0' => array1.merge( { 'members' => expected_disks.keys[0..4].map { |x| "#{x}1" } }, ), '/dev/md1' => array2.merge( { 'members' => expected_disks.keys[5..9].map { |x| "#{x}1" } }, ), '/dev/md2' => array3.merge( { 'members' => expected_disks.keys[10..14].map { |x| "#{x}1" } }, ), } expected = { :disks => expected_disks, :arrays => expected_arrays } expect(FB::Storage.build_mapping(node, [])).to eq(expected) end it 'should build a config with multiple array mapping, ' + 'and a skipped array' do node.default['fb_storage']['devices'] = [ { '_skip' => true }, { '_skip' => true }, { '_skip' => true }, { '_skip' => true }, { '_skip' => true }, device6, device7, device8, device9, device10, device11, device12, device13, device14, device15 ] node.default['fb_storage']['arrays'] = [ { '_skip' => true }, array2, array3 ] expected_disks = { '/dev/sdb' => { '_skip' => true }, '/dev/sdc' => { '_skip' => true }, '/dev/sdd' => { '_skip' => true }, '/dev/sde' => { '_skip' => true }, '/dev/sdf' => { '_skip' => true }, '/dev/sdg' => device6, '/dev/sdh' => device7, '/dev/sdi' => device8, '/dev/sdj' => device9, '/dev/sdk' => device10, '/dev/sdl' => device11, '/dev/sdm' => device12, '/dev/sdn' => device13, '/dev/sdo' => device15, '/dev/sdp' => device15, } expected_disks.each_key do |d| node.automatic['block_device'][File.basename(d)] = {} end node.automatic['block_device']['sda'] = {} expected_arrays = { '/dev/md0' => { '_skip' => true, 'members' => [] }, '/dev/md1' => array2.merge( { 'members' => expected_disks.keys[5..9].map { |x| "#{x}1" } }, ), '/dev/md2' => array3.merge( { 'members' => expected_disks.keys[10..14].map { |x| "#{x}1" } }, ), } expected = { :disks => expected_disks, :arrays => expected_arrays } expect(FB::Storage.build_mapping(node, [])).to eq(expected) end it 'should build a config with multiple array mapping, ' + 'and / and /boot on their own RAID devices' do node.default['fb_storage']['devices'] = [ device6, device7, device8, device9, device10, device11, device12, device13, device14, device15 ] node.default['fb_storage']['arrays'] = [ # / is md0, need to skip it { '_skip' => true }, array2, array3 ] expected_disks = { '/dev/sdg' => device6, '/dev/sdh' => device7, '/dev/sdi' => device8, '/dev/sdj' => device9, '/dev/sdk' => device10, '/dev/sdl' => device11, '/dev/sdm' => device12, '/dev/sdn' => device13, '/dev/sdo' => device15, '/dev/sdp' => device15, } ('sda'..'sdp').each do |d| node.automatic['block_device'][d] = {} end node.automatic['block_device']['md0'] = {} node.automatic['block_device']['md21'] = {} { '/' => '/dev/md0', '/boot' => '/dev/md21', }.each do |mp, device| allow(node).to receive(:device_of_mount).with(mp).and_return(device) allow(File).to receive(:symlink?).with(device).and_return(false) if mp == '/boot' boot_mock = double allow(Pathname).to receive(:new).with(mp).and_return(boot_mock) allow(boot_mock).to receive(:mountpoint?).and_return(device) end end allow(Dir).to receive(:glob).with('/sys/block/md0/slaves/*').and_return( ('sdc'..'sdf').map { |disk| "/sys/block/md0/slaves/#{disk}" }, ) allow(Dir).to receive(:glob).with('/sys/block/md21/slaves/*'). and_return( %w{sda sdb}.map { |disk| "/sys/block/md21/slaves/#{disk}" }, ) expected_arrays = { '/dev/md0' => { '_skip' => true, 'members' => [] }, '/dev/md1' => array2.merge( { 'members' => expected_disks.keys[0..4].map { |x| "#{x}1" } }, ), '/dev/md21' => { '_skip' => true, 'members' => [] }, '/dev/md2' => array3.merge( { 'members' => expected_disks.keys[5..9].map { |x| "#{x}1" } }, ), } expected = { :disks => expected_disks, :arrays => expected_arrays } expect(FB::Storage.build_mapping(node, [])).to eq(expected) end it 'should build a config / and /boot on their own RAID devices' do node.default['fb_storage']['devices'] = [] ('sda'..'sdp').each do |d| node.automatic['block_device'][d] = {} end node.automatic['block_device']['md0'] = {} node.automatic['block_device']['md21'] = {} { '/' => '/dev/md0', '/boot' => '/dev/md21', }.each do |mp, device| allow(node).to receive(:device_of_mount).with(mp).and_return(device) allow(File).to receive(:symlink?).with(device).and_return(false) end allow(Dir).to receive(:glob).with('/sys/block/md0/slaves/*').and_return( ('sda'..'sdf').map { |disk| "/sys/block/md0/slaves/#{disk}" }, ) allow(Dir).to receive(:glob).with('/sys/block/md21/slaves/*'). and_return( ('sdg'..'sdp').map { |disk| "/sys/block/md21/slaves/#{disk}" }, ) expected_arrays = { '/dev/md0' => { '_skip' => true, 'members' => [] }, '/dev/md21' => { '_skip' => true, 'members' => [] }, } expected = { :disks => {}, :arrays => expected_arrays } expect(FB::Storage.build_mapping(node, [])).to eq(expected) end it 'should fail to build a config with an array and / both on md0' do node.default['fb_storage']['devices'] = [ device1, device2, device3, device4, device5, device6, device7, device8, device9, device10, device11, device12, device13, device14, device15 ] node.default['fb_storage']['arrays'] = [ array1, array2, array3 ] ('sda'..'sdp').each do |d| node.automatic['block_device'][d] = {} end node.automatic['block_device']['md0'] = {} node.automatic['block_device']['nvme0'] = {} { '/' => '/dev/md0', '/boot' => '/dev/md0', }.each do |mp, device| allow(node).to receive(:device_of_mount).with(mp).and_return(device) allow(File).to receive(:symlink?).with(device).and_return(false) if mp == '/boot' boot_mock = double allow(Pathname).to receive(:new).with(mp).and_return(boot_mock) allow(boot_mock).to receive(:mountpoint?).and_return(device) end end allow(Dir).to receive(:glob).with('/sys/block/md0/slaves/*').and_return( ['sda', 'nvme0'].map { |disk| "/sys/block/md0/slaves/#{disk}" }, ) expect do FB::Storage.build_mapping(node, []) end.to raise_error( RuntimeError, 'fb_storage: Asked to configure md0 but that is `/`!' ) end it 'should build a config without /boot mounted' do node.default['fb_storage']['devices'] = [ device1, device2, device3 ] node.default['fb_storage']['arrays'] = [array1] ('sda'..'sdd').each do |d| node.automatic['block_device'][d] = {} end root_dev = 'md4' node.automatic['block_device'][root_dev] = {} node.automatic['block_device']['nvme0'] = {} root_md = "/dev/#{root_dev}" allow(node).to receive(:device_of_mount).with('/').and_return(root_md) allow(File).to receive(:symlink?).with(root_md).and_return(false) allow(Dir).to receive(:glob).with("/sys/block/#{root_dev}/slaves/*"). and_return( ['sda', 'nvme0'].map do |disk| "/sys/block/#{root_dev}/slaves/#{disk}" end, ) # Because we do Pathname.new('/boot'), we need to mock it boot_mock = double allow(Pathname).to receive(:new).with('/boot').and_return(boot_mock) allow(boot_mock).to receive(:mountpoint?).and_return(nil) expected_disks = { '/dev/sdb' => device1, '/dev/sdc' => device2, '/dev/sdd' => device3, } expected_arrays = { '/dev/md0' => array1.merge( { 'members' => expected_disks.keys.map { |x| "#{x}1" } }, ), root_md => { '_skip' => true, 'members' => [] }, } expected = { :disks => expected_disks, :arrays => expected_arrays } expect(FB::Storage.build_mapping(node, [])).to eq(expected) end end context 'hybrid_xfs arrays' do let(:device1) do { 'partitions' => 3.times.map do |i| { '_xfs_rt_metadata' => i, } end, } end 3.times.each do |i| let("device#{i + 2}".to_sym) do { 'partitions' => [ { '_xfs_rt_data' => i, }, ], } end let("array#{i + 1}".to_sym) do { 'type' => 'xfs', 'mount_point' => "/data/#{i + 1}", 'opts' => 'default', 'pass' => 2, 'enable_remount' => true, 'raid_level' => 'hybrid_xfs', } end end it 'should build a config with hybrid_xfs mapping' do node.default['fb_storage']['devices'] = [ device1, device2, device3, device4 ] node.default['fb_storage']['arrays'] = [ array1, array2, array3 ] node.automatic['block_device'] = { 'sda' => {}, 'sdb' => {}, 'sdc' => {}, 'sdd' => {}, 'sde' => {}, } expected_disks = { '/dev/sdb' => device1, '/dev/sdc' => device2, '/dev/sdd' => device3, '/dev/sde' => device4, } %w{sdc sdd sde}.each_with_index do |d, idx| expected_disks["/dev/#{d}"]['partitions'][0]['part_name'] = "/data/#{idx + 1}" end 3.times.map do |i| expected_disks['/dev/sdb']['partitions'][i]['part_name'] = "md:/data/#{i + 1}" end node.automatic['block_device']['sda'] = {} expected_arrays = { '/dev/md0' => array1.merge( { 'members' => ['/dev/sdc1'], 'journal' => '/dev/sdb1', }, ), '/dev/md1' => array2.merge( { 'members' => ['/dev/sdd1'], 'journal' => '/dev/sdb2', }, ), '/dev/md2' => array3.merge( { 'members' => ['/dev/sde1'], 'journal' => '/dev/sdb3', }, ), } expected = { :disks => expected_disks, :arrays => expected_arrays } expect(FB::Storage.build_mapping(node, [])).to eq(expected) end end end context '#partition_names' do it 'should list all configured partitions for a device' do device1 = { 'partitions' => [ { 'type' => 'xfs', 'mount_point' => '/data/1', 'opts' => 'default', 'pass' => 2, 'enable_remount' => true, }, ], } device2 = { 'partitions' => [ { 'type' => 'xfs', 'mount_point' => '/waka/1', 'opts' => 'default', 'pass' => 2, 'enable_remount' => true, }, { 'type' => 'xfs', 'mount_point' => '/waka/2', 'opts' => 'default', 'pass' => 2, 'enable_remount' => true, }, { 'type' => 'xfs', 'mount_point' => '/waka/3', 'opts' => 'default', 'pass' => 2, 'enable_remount' => true, }, ], } FB::Storage.partition_names('/dev/fioa', device1). should eq(['/dev/fioa1']) FB::Storage.partition_names('/dev/fioa', device2). should eq(['/dev/fioa1', '/dev/fioa2', '/dev/fioa3']) end end context 'common device needs' do let(:single_partition_device) do { 'partitions' => [ { 'type' => 'xfs', 'mount_point' => '/data/fa', 'opts' => 'default', 'pass' => 2, 'enable_remount' => true, 'label' => '/data/fa', }, ], } end let(:double_partition_device) do { 'partitions' => [ { 'type' => 'xfs', 'mount_point' => '/data/fa2', 'opts' => 'default', 'pass' => 2, 'enable_remount' => true, }, { 'type' => 'xfs', 'mount_point' => '/data/fa3', 'opts' => 'default', 'pass' => 2, 'enable_remount' => true, }, ], } end let(:single_array) do { 'type' => 'xfs', 'mount_point' => '/data/fa', 'opts' => 'default', 'pass' => 2, 'enable_remount' => true, 'raid_level' => 1, } end let(:raid0_array) do { 'type' => 'xfs', 'mount_point' => '/data/flash', 'opts' => 'default', 'pass' => 2, 'enable_remount' => true, 'raid_level' => 0, } end let(:array_member) do { 'partitions' => [ { '_swraid_array' => 0, }, ], } end let(:whole_device_single_fs) do { 'whole_device' => true, 'partitions' => [{ 'type' => 'xfs', 'mount_point' => '/data/fa', 'opts' => 'default', 'pass' => 2, 'enable_remount' => true, 'label' => '/data/fa', }], } end context '#out_of_spec' do before do allow(FB::Storage).to receive(:get_actual_part_name).and_return( nil, # '' ) end it 'should return nothing if everything is in spec' do node.default['fb_storage']['devices'] = [ single_partition_device, double_partition_device, whole_device_single_fs ] node.automatic[attr_name]['by_device'] = { '/dev/sdb1' => { 'fs_type' => 'xfs', 'mounts' => ['/data/fa'], 'label' => '/data/fa', }, '/dev/sdc1' => { 'fs_type' => 'xfs', 'mounts' => ['/data/fa'], }, '/dev/sdc2' => { 'fs_type' => 'xfs', 'mounts' => ['/data/fa'], }, '/dev/sdd' => { 'fs_type' => 'xfs', 'mounts' => ['/data/fa'], 'label' => '/data/fa', }, } expect(FB::Storage).to receive(:build_mapping).and_return( { :disks => { '/dev/sdb' => single_partition_device, '/dev/sdc' => double_partition_device, }, :arrays => {}, }, ) storage = FB::Storage.new(node) expect(storage.out_of_spec).to eq( { :mismatched_partitions => [], :mismatched_filesystems => [], :mismatched_arrays => [], :missing_partitions => [], :missing_filesystems => [], :missing_arrays => [], :incomplete_arrays => {}, :extra_arrays => [], }, ) end it 'should identify mismatched filesystems' do node.default['fb_storage']['devices'] = [ single_partition_device, ] node.automatic[attr_name]['by_device'] = { '/dev/sdb1' => { 'fs_type' => 'ext4', 'mounts' => ['/data/fa'], }, } expect(FB::Storage).to receive(:build_mapping).and_return( { :disks => { '/dev/sdb' => single_partition_device }, :arrays => {} }, ) storage = FB::Storage.new(node) expect(storage.out_of_spec).to eq( { :mismatched_partitions => [], :mismatched_filesystems => ['/dev/sdb1'], :mismatched_arrays => [], :missing_partitions => [], :missing_filesystems => [], :missing_arrays => [], :incomplete_arrays => {}, :extra_arrays => [], }, ) end it 'should identify mismatched partitions' do node.default['fb_storage']['devices'] = [ single_partition_device, ] node.automatic[attr_name]['by_device'] = { '/dev/sdb1' => { 'fs_type' => 'xfs', 'mounts' => ['/data/fa'], }, '/dev/sdb2' => { 'fs_type' => 'xfs', 'mounts' => ['/data/fa'], }, } expect(FB::Storage).to receive(:build_mapping).and_return( { :disks => { '/dev/sdb' => single_partition_device }, :arrays => {} }, ) storage = FB::Storage.new(node) expect(storage.out_of_spec).to eq( { :mismatched_partitions => ['/dev/sdb'], :mismatched_filesystems => ['/dev/sdb1'], :mismatched_arrays => [], :missing_partitions => [], :missing_filesystems => [], :missing_arrays => [], :incomplete_arrays => {}, :extra_arrays => [], }, ) end it 'should identify missing partitions' do node.default['fb_storage']['devices'] = [ double_partition_device, ] node.automatic[attr_name]['by_device'] = { '/dev/sdb' => {}, } expect(FB::Storage).to receive(:build_mapping).and_return( { :disks => { '/dev/sdb' => double_partition_device }, :arrays => {} }, ) storage = FB::Storage.new(node) expect(storage.out_of_spec).to eq( { :mismatched_partitions => [], :mismatched_filesystems => [], :mismatched_arrays => [], :missing_partitions => ['/dev/sdb'], :missing_filesystems => ['/dev/sdb1', '/dev/sdb2'], :missing_arrays => [], :incomplete_arrays => {}, :extra_arrays => [], }, ) end it 'should identify mismatched labels on a partition' do node.default['fb_storage']['devices'] = [ single_partition_device, ] node.automatic[attr_name]['by_device'] = { '/dev/sdb1' => { 'fs_type' => 'xfs', 'mounts' => ['/data/fa'], 'label' => '/wrong_label', }, } expect(FB::Storage).to receive(:build_mapping).and_return( { :disks => { '/dev/sdb' => single_partition_device }, :arrays => {} }, ) storage = FB::Storage.new(node) expect(storage.out_of_spec).to eq( { :mismatched_partitions => [], :mismatched_filesystems => ['/dev/sdb1'], :mismatched_arrays => [], :missing_partitions => [], :missing_filesystems => [], :missing_arrays => [], :incomplete_arrays => {}, :extra_arrays => [], }, ) end it 'should identify wrong partlabel on hybrid metadata device' do device1 = { 'partitions' => [ { '_xfs_rt_metadata' => 0, 'part_name' => 'wrong_part_name', }, ], } device2 = { 'partitions' => [ { '_xfs_rt_data' => 0, 'part_name' => 'correct_part_name', }, { '_xfs_rt_rescue' => 0, 'part_name' => 'correct_part_name', }, ], } array1 = { 'type' => 'xfs', 'raid_level' => 'hybrid_xfs', 'mount_point' => '/data/fa', } node.default['fb_storage']['devices'] = [device1, device2] node.default['fb_storage']['arrays'] = [array1] node.automatic[attr_name]['by_device'] = { '/dev/sdb' => {}, '/dev/sdb1' => { 'fs_type' => 'xfs' }, '/dev/sdc' => {}, '/dev/sdc1' => {}, '/dev/sdc2' => {}, } node.automatic['mdadm'] = {} expect(FB::Storage).to receive(:build_mapping).and_return( { :disks => { '/dev/sdb' => device1, '/dev/sdc' => device2 }, :arrays => { '/dev/md0' => array1.merge( { 'members' => ['/dev/sdc1'], 'journal' => '/dev/sdb1' }, ), }, }, ) expect(FB::Storage).to receive(:get_actual_part_name).and_return( 'correct_part_name', ).exactly(3).times storage = FB::Storage.new(node) expect(storage.out_of_spec).to eq( { :mismatched_partitions => ['/dev/sdb'], :mismatched_filesystems => [], :mismatched_arrays => [], :missing_partitions => [], :missing_filesystems => [], :missing_arrays => [], :incomplete_arrays => {}, :extra_arrays => [], }, ) end it 'should not consider hybrid md out of spec when partlabel correct' do device1 = { 'partitions' => [ { '_xfs_rt_metadata' => 0, 'part_name' => 'correct_part_name', }, ], } device2 = { 'partitions' => [ { '_xfs_rt_data' => 0, 'part_name' => 'correct_part_name', }, { '_xfs_rt_rescue' => 0, 'part_name' => 'correct_part_name', }, ], } array1 = { 'type' => 'xfs', 'raid_level' => 'hybrid_xfs', 'mount_point' => '/data/fa', } node.default['fb_storage']['devices'] = [device1, device2] node.default['fb_storage']['arrays'] = [array1] node.automatic[attr_name]['by_device'] = { '/dev/sdb' => {}, '/dev/sdb1' => { 'fs_type' => 'xfs' }, '/dev/sdc' => {}, '/dev/sdc1' => {}, '/dev/sdc2' => {}, } node.automatic['mdadm'] = {} expect(FB::Storage).to receive(:build_mapping).and_return( { :disks => { '/dev/sdb' => device1, '/dev/sdc' => device2 }, :arrays => { '/dev/md0' => array1.merge( { 'members' => ['/dev/sdc1'], 'journal' => '/dev/sdb1' }, ), }, }, ) expect(FB::Storage).to receive(:get_actual_part_name).and_return( 'correct_part_name', ).exactly(3).times storage = FB::Storage.new(node) expect(storage.out_of_spec).to eq( { :mismatched_partitions => [], :mismatched_filesystems => [], :mismatched_arrays => [], :missing_partitions => [], :missing_filesystems => [], :missing_arrays => [], :incomplete_arrays => {}, :extra_arrays => [], }, ) end it 'should flag wrong xfs label on hybrid md device as out of spec' do device1 = { 'partitions' => [ { '_xfs_rt_metadata' => 0, 'part_name' => 'correct_part_name', }, ], } device2 = { 'partitions' => [ { '_xfs_rt_data' => 0, 'part_name' => 'correct_part_name', }, { '_xfs_rt_rescue' => 0, 'part_name' => 'correct_part_name', }, ], } array1 = { 'type' => 'xfs', 'raid_level' => 'hybrid_xfs', 'mount_point' => '/data/fa', } node.default['fb_storage']['devices'] = [device1, device2] node.default['fb_storage']['arrays'] = [array1] node.automatic[attr_name]['by_device'] = { '/dev/sdb' => {}, '/dev/sdb1' => { 'fs_type' => 'xfs', 'label' => '/wrong_label' }, '/dev/sdc' => {}, '/dev/sdc1' => {}, '/dev/sdc2' => {}, } node.automatic['mdadm'] = {} expect(FB::Storage).to receive(:build_mapping).and_return( { :disks => { '/dev/sdb' => device1, '/dev/sdc' => device2 }, :arrays => { '/dev/md0' => array1.merge( { 'members' => ['/dev/sdc1'], 'journal' => '/dev/sdb1', 'label' => '/data/fa' }, ), }, }, ) expect(FB::Storage).to receive(:get_actual_part_name).and_return( 'correct_part_name', ).exactly(3).times storage = FB::Storage.new(node) expect(storage.out_of_spec).to eq( { :mismatched_partitions => [], :mismatched_filesystems => ['/dev/md0', '/dev/sdb1'], :mismatched_arrays => [], :missing_partitions => [], :missing_filesystems => [], :missing_arrays => [], :incomplete_arrays => {}, :extra_arrays => [], }, ) end it 'should not flag a hybrid xfs md partition with the correct label' do device1 = { 'partitions' => [ { '_xfs_rt_metadata' => 0, 'part_name' => 'correct_part_name', }, ], } device2 = { 'partitions' => [ { '_xfs_rt_data' => 0, 'part_name' => 'correct_part_name', }, { '_xfs_rt_rescue' => 0, 'part_name' => 'correct_part_name', }, ], } array1 = { 'type' => 'xfs', 'raid_level' => 'hybrid_xfs', 'mount_point' => '/data/fa', } node.default['fb_storage']['devices'] = [device1, device2] node.default['fb_storage']['arrays'] = [array1] node.automatic[attr_name]['by_device'] = { '/dev/sdb' => {}, '/dev/sdb1' => { 'fs_type' => 'xfs', 'label' => '/data/fa' }, '/dev/sdc' => {}, '/dev/sdc1' => {}, '/dev/sdc2' => {}, } node.automatic['mdadm'] = {} expect(FB::Storage).to receive(:build_mapping).and_return( { :disks => { '/dev/sdb' => device1, '/dev/sdc' => device2 }, :arrays => { '/dev/md0' => array1.merge( { 'members' => ['/dev/sdc1'], 'journal' => '/dev/sdb1', 'label' => '/data/fa' }, ), }, }, ) expect(FB::Storage).to receive(:get_actual_part_name).and_return( 'correct_part_name', ).exactly(3).times storage = FB::Storage.new(node) expect(storage.out_of_spec).to eq( { :mismatched_partitions => [], :mismatched_filesystems => [], :mismatched_arrays => [], :missing_partitions => [], :missing_filesystems => [], :missing_arrays => [], :incomplete_arrays => {}, :extra_arrays => [], }, ) end it 'should identify mismatched partlabels for hybrid FS' do device1 = { 'partitions' => [ { '_xfs_rt_metadata' => 0, 'part_name' => 'correct_part_name', }, ], } device2 = { 'partitions' => [ { '_xfs_rt_data' => 0, 'part_name' => 'wrong_part_name', }, { '_xfs_rt_rescue' => 0, 'part_name' => 'correct_part_name', }, ], } array1 = { 'type' => 'xfs', 'raid_level' => 'hybrid_xfs', 'mount_point' => '/data/fa', } node.default['fb_storage']['devices'] = [device1, device2] node.default['fb_storage']['arrays'] = [array1] node.automatic[attr_name]['by_device'] = { '/dev/sdb' => {}, '/dev/sdb1' => { 'fs_type' => 'xfs' }, '/dev/sdc' => {}, '/dev/sdc1' => {}, '/dev/sdc2' => {}, } node.automatic['mdadm'] = {} expect(FB::Storage).to receive(:build_mapping).and_return( { :disks => { '/dev/sdb' => device1, '/dev/sdc' => device2 }, :arrays => { '/dev/md0' => array1.merge( { 'members' => ['/dev/sdc1'], 'journal' => '/dev/sdb1' }, ), }, }, ) expect(FB::Storage).to receive(:get_actual_part_name).and_return( 'correct_part_name', ).exactly(3).times storage = FB::Storage.new(node) expect(storage.out_of_spec).to eq( { :mismatched_partitions => ['/dev/sdc'], :mismatched_filesystems => [], :mismatched_arrays => [], :missing_partitions => [], :missing_filesystems => [], :missing_arrays => [], :incomplete_arrays => {}, :extra_arrays => [], }, ) end it 'it should match correct partlabels for hybrid FS' do device1 = { 'partitions' => [ { '_xfs_rt_metadata' => 0, 'part_name' => 'correct_part_name', }, ], } device2 = { 'partitions' => [ { '_xfs_rt_data' => 0, 'part_name' => 'correct_part_name', }, { '_xfs_rt_rescue' => 0, 'part_name' => 'correct_part_name', }, ], } array1 = { 'type' => 'xfs', 'raid_level' => 'hybrid_xfs', 'mount_point' => '/data/fa', } node.default['fb_storage']['devices'] = [device1, device2] node.default['fb_storage']['arrays'] = [array1] node.automatic[attr_name]['by_device'] = { '/dev/sdb' => {}, '/dev/sdb1' => { 'fs_type' => 'xfs' }, '/dev/sdc' => {}, '/dev/sdc1' => {}, '/dev/sdc2' => {}, } node.automatic['mdadm'] = {} expect(FB::Storage).to receive(:build_mapping).and_return( { :disks => { '/dev/sdb' => device1, '/dev/sdc' => device2 }, :arrays => { '/dev/md0' => array1.merge( { 'members' => ['/dev/sdc1'], 'journal' => '/dev/sdb1' }, ), }, }, ) expect(FB::Storage).to receive(:get_actual_part_name).and_return( 'correct_part_name', ).exactly(3).times storage = FB::Storage.new(node) expect(storage.out_of_spec).to eq( { :mismatched_partitions => [], :mismatched_filesystems => [], :mismatched_arrays => [], :missing_partitions => [], :missing_filesystems => [], :missing_arrays => [], :incomplete_arrays => {}, :extra_arrays => [], }, ) end it 'should identify mismatched labels on a whole device' do node.default['fb_storage']['devices'] = [ whole_device_single_fs, ] node.automatic[attr_name]['by_device'] = { '/dev/sdb' => { 'fs_type' => 'xfs', 'mounts' => ['/data/fa'], 'label' => '/wrong_label', }, } expect(FB::Storage).to receive(:build_mapping).and_return( { :disks => { '/dev/sdb' => whole_device_single_fs }, :arrays => {} }, ) storage = FB::Storage.new(node) puts(storage.out_of_spec) expect(storage.out_of_spec).to eq( { :mismatched_partitions => [], :mismatched_filesystems => ['/dev/sdb'], :mismatched_arrays => [], :missing_partitions => [], :missing_filesystems => [], :missing_arrays => [], :incomplete_arrays => {}, :extra_arrays => [], }, ) end it 'should identify missing arrays' do node.default['fb_storage']['devices'] = [ array_member, array_member ] node.default['fb_storage']['arrays'] = [single_array] node.automatic[attr_name]['by_device'] = { '/dev/sdb' => {}, '/dev/sdc' => {}, } expect(FB::Storage).to receive(:build_mapping).and_return( { :disks => { '/dev/sdb' => array_member, '/dev/sdc' => array_member }, :arrays => { '/dev/md0' => single_array }, }, ) storage = FB::Storage.new(node) expect(storage.out_of_spec).to eq( { :mismatched_partitions => [], :mismatched_filesystems => ['/dev/md0'], :mismatched_arrays => [], :missing_partitions => ['/dev/sdb', '/dev/sdc'], :missing_filesystems => ['/dev/sdb1', '/dev/sdc1'], :missing_arrays => ['/dev/md0'], :incomplete_arrays => {}, :extra_arrays => [], }, ) end it 'should identify mismatched arrays' do device1 = { '_skip' => true, } node.default['fb_storage']['devices'] = [ device1, array_member, array_member ] node.default['fb_storage']['arrays'] = [single_array] node.automatic[attr_name]['by_device'] = { '/dev/sdb' => {}, '/dev/sdc' => {}, '/dev/sdd' => {}, '/dev/md0' => { 'fs_type' => 'xfs' }, } node.automatic['mdadm']['md0'] = { 'level' => 1, 'members' => ['sdb1', 'sdc1'], } expect(FB::Storage).to receive(:build_mapping).and_return( { :disks => { '/dev/sdb' => device1, '/dev/sdc' => array_member, '/dev/sdd' => array_member, }, :arrays => { '/dev/md0' => single_array.merge( { 'members' => ['/dev/sdc1', '/dev/sdd1'] }, ), }, }, ) storage = FB::Storage.new(node) expect(storage.out_of_spec).to eq( { :mismatched_partitions => [], :mismatched_filesystems => ['/dev/md0'], :mismatched_arrays => ['/dev/md0'], :missing_partitions => ['/dev/sdc', '/dev/sdd'], :missing_filesystems => ['/dev/sdc1', '/dev/sdd1'], :missing_arrays => [], :incomplete_arrays => {}, :extra_arrays => [], }, ) end it 'should identify arrays with missing members' do node.default['fb_storage']['devices'] = [ array_member, array_member ] node.default['fb_storage']['arrays'] = [single_array] node.automatic[attr_name]['by_device'] = { '/dev/sdb' => {}, '/dev/sdc' => {}, '/dev/md0' => { 'fs_type' => 'xfs' }, } node.automatic['mdadm']['md0'] = { 'level' => 1, 'members' => ['sdb1'], } expect(FB::Storage).to receive(:build_mapping).and_return( { :disks => { '/dev/sdb' => array_member, '/dev/sdc' => array_member }, :arrays => { '/dev/md0' => single_array.merge( { 'members' => ['/dev/sdb1', '/dev/sdc1'] }, ), }, }, ) storage = FB::Storage.new(node) expect(storage.out_of_spec).to eq( { :mismatched_partitions => [], :mismatched_filesystems => [], :mismatched_arrays => [], :missing_partitions => ['/dev/sdb', '/dev/sdc'], :missing_filesystems => ['/dev/sdb1', '/dev/sdc1'], :missing_arrays => [], :incomplete_arrays => { '/dev/md0' => ['/dev/sdc1'] }, :extra_arrays => [], }, ) end it 'should identify partial RAID0 as mismatched when md is broken' do # When all of the storage devices are already set up, but something # goes wrong with assembling the md device, we should report a mismatch # For this case there's no functional fs on top of the md device # because a missing or broken device breaks RAID0. We can have the right # devices be visible, but md fail to assemble the device node.default['fb_storage']['devices'] = [ array_member, array_member, array_member ] node.default['fb_storage']['arrays'] = [raid0_array] node.automatic[attr_name]['by_device'] = { '/dev/nbd0p1' => { :fs_type => 'linux_raid_member' }, '/dev/nbd1p1' => { :fs_type => 'linux_raid_member' }, '/dev/nbd2p1' => { :fs_type => 'linux_raid_member' }, # md0 exists, but is inactive, so no visible fs '/dev/md0' => {}, } node.automatic['mdadm']['md0'] = { 'level' => 0, # inactive RAID0 has empty members 'members' => [], } expect(FB::Storage).to receive(:build_mapping).and_return( { :disks => { '/dev/nbd0' => array_member, '/dev/nbd1' => array_member, '/dev/nbd2' => array_member, }, :arrays => { '/dev/md0' => raid0_array.merge( { 'members' => ['/dev/nbd0p1', '/dev/nbd1p1', '/dev/nbd2p1'] }, ), }, }, ) storage = FB::Storage.new(node) expect(storage.out_of_spec).to eq( { :mismatched_partitions => [], :mismatched_filesystems => ['/dev/md0'], :mismatched_arrays => ['/dev/md0'], :missing_partitions => [], :missing_filesystems => [], :missing_arrays => [], :incomplete_arrays => {}, :extra_arrays => [], }, ) end it 'should identify mismatched raid levels' do # If we want a stripe, but have a mirror, it's a mismatch node.default['fb_storage']['devices'] = [ array_member, array_member, array_member ] node.default['fb_storage']['arrays'] = [raid0_array] node.automatic[attr_name]['by_device'] = { '/dev/nbd0p1' => { :fs_type => 'linux_raid_member' }, '/dev/nbd1p1' => { :fs_type => 'linux_raid_member' }, '/dev/nbd2p1' => { :fs_type => 'linux_raid_member' }, '/dev/md0' => { 'fs_type' => 'xfs' }, } node.automatic['mdadm']['md0'] = { 'level' => 1, 'members' => ['nbd0p1', 'nbd1p1'], } expect(FB::Storage).to receive(:build_mapping).and_return( { :disks => { '/dev/nbd0' => array_member, '/dev/nbd1' => array_member, '/dev/nbd2' => array_member, }, :arrays => { '/dev/md0' => raid0_array.merge( { 'members' => ['/dev/nbd0p1', '/dev/nbd1p1', '/dev/nbd2p1'] }, ), }, }, ) storage = FB::Storage.new(node) expect(storage.out_of_spec).to eq( { :mismatched_partitions => [], :mismatched_filesystems => ['/dev/md0'], :mismatched_arrays => ['/dev/md0'], :missing_partitions => [], :missing_filesystems => [], :missing_arrays => [], :incomplete_arrays => {}, :extra_arrays => [], }, ) end it 'should identify arrays with entirely wrong members as a mismatch' do # If we have an array with A,B,C, but it should be D,E,F => mismatch node.default['fb_storage']['devices'] = [ array_member, array_member, array_member ] node.default['fb_storage']['arrays'] = [raid0_array] node.automatic[attr_name]['by_device'] = { '/dev/nbd0p1' => { :fs_type => 'linux_raid_member' }, '/dev/nbd1p1' => { :fs_type => 'linux_raid_member' }, '/dev/nbd2p1' => { :fs_type => 'linux_raid_member' }, } node.automatic['mdadm']['md0'] = { 'level' => 0, 'members' => ['nbd3p1', 'nbd4p1', 'nbd5p1'], } expect(FB::Storage).to receive(:build_mapping).and_return( { :disks => { '/dev/nbd0' => array_member, '/dev/nbd1' => array_member, '/dev/nbd2' => array_member, }, :arrays => { '/dev/md0' => raid0_array.merge( { 'members' => ['/dev/nbd0p1', '/dev/nbd1p1', '/dev/nbd2p1'] }, ), }, }, ) storage = FB::Storage.new(node) expect(storage.out_of_spec).to eq( { :mismatched_partitions => [], :mismatched_filesystems => ['/dev/md0'], :mismatched_arrays => ['/dev/md0'], :missing_partitions => [], :missing_filesystems => [], :missing_arrays => [], :incomplete_arrays => {}, :extra_arrays => [], }, ) end it 'should identify extra arrays' do device1 = { 'partitions' => [ { '_swraid_array' => 0, }, ], } device2 = { 'partitions' => [ { '_swraid_array' => 0, }, ], } array1 = { 'type' => 'xfs', 'mount_point' => '/data/fa', 'opts' => 'default', 'pass' => 2, 'enable_remount' => true, 'raid_level' => 1, } node.default['fb_storage']['devices'] = [device1, device2] node.default['fb_storage']['arrays'] = [array1] node.automatic[attr_name]['by_device'] = { '/dev/sdb' => {}, '/dev/sdb1' => { 'fs_type' => 'xfs' }, '/dev/sdc' => {}, '/dev/sdc1' => { 'fs_type' => 'xfs' }, '/dev/md0' => { 'fs_type' => 'xfs' }, } node.automatic['mdadm'] = { 'md0' => { 'level' => 1, 'members' => ['sdb1', 'sdc1'], }, 'md1' => { 'level' => 6, 'members' => ['whatever'], }, } expect(FB::Storage).to receive(:build_mapping).and_return( { :disks => { '/dev/sdb' => device1, '/dev/sdc' => device2 }, :arrays => { '/dev/md0' => array1.merge( { 'members' => ['/dev/sdb1', '/dev/sdc1'] }, ), }, }, ) storage = FB::Storage.new(node) expect(storage.out_of_spec).to eq( { :mismatched_partitions => [], :mismatched_filesystems => [], :mismatched_arrays => [], :missing_partitions => [], :missing_filesystems => [], :missing_arrays => [], :incomplete_arrays => {}, :extra_arrays => ['/dev/md1'], }, ) end it 'it match hybrid_xfs metadata as xfs for matched FS' do device1 = { 'partitions' => [ { '_xfs_rt_metadata' => 0, }, ], } device2 = { 'partitions' => [ { '_xfs_rt_data' => 0, }, ], } array1 = { 'type' => 'xfs', 'raid_level' => 'hybrid_xfs', 'mount_point' => '/data/fa', } node.default['fb_storage']['devices'] = [device1, device2] node.default['fb_storage']['arrays'] = [array1] node.automatic[attr_name]['by_device'] = { '/dev/sdb' => {}, '/dev/sdb1' => { 'fs_type' => 'xfs' }, '/dev/sdc' => {}, '/dev/sdc1' => {}, } node.automatic['mdadm'] = {} expect(FB::Storage).to receive(:build_mapping).and_return( { :disks => { '/dev/sdb' => device1, '/dev/sdc' => device2 }, :arrays => { '/dev/md0' => array1.merge( { 'members' => ['/dev/sdc1'], 'journal' => '/dev/sdb1' }, ), }, }, ) storage = FB::Storage.new(node) expect(storage.out_of_spec).to eq( { :mismatched_partitions => [], :mismatched_filesystems => [], :mismatched_arrays => [], :missing_partitions => [], :missing_filesystems => [], :missing_arrays => [], :incomplete_arrays => {}, :extra_arrays => [], }, ) end it 'matches hybrid_xfs metadata as xfs for mismatched FSes' do device1 = { 'partitions' => [ { '_xfs_rt_metadata' => 0, }, ], } device2 = { 'partitions' => [ { '_xfs_rt_data' => 0, }, ], } array1 = { 'type' => 'xfs', 'raid_level' => 'hybrid_xfs', 'mount_point' => '/data/fa', } node.default['fb_storage']['devices'] = [device1, device2] node.default['fb_storage']['arrays'] = [array1] node.automatic[attr_name]['by_device'] = { '/dev/sdb' => {}, '/dev/sdb1' => { 'fs_type' => 'ext4' }, '/dev/sdc' => {}, '/dev/sdc1' => {}, } node.automatic['mdadm'] = {} expect(FB::Storage).to receive(:build_mapping).and_return( { :disks => { '/dev/sdb' => device1, '/dev/sdc' => device2 }, :arrays => { '/dev/md0' => array1.merge( { 'members' => ['/dev/sdc1'], 'journal' => '/dev/sdb1' }, ), }, }, ) storage = FB::Storage.new(node) expect(storage.out_of_spec).to eq( { :mismatched_partitions => [], :mismatched_filesystems => ['/dev/md0', '/dev/sdb1'], :mismatched_arrays => [], :missing_partitions => [], :missing_filesystems => [], :missing_arrays => [], :incomplete_arrays => {}, :extra_arrays => [], }, ) end it 'matches hybrid_xfs metadata as xfs for missing FSes' do device1 = { 'partitions' => [ { '_xfs_rt_metadata' => 0, }, ], } device2 = { 'partitions' => [ { '_xfs_rt_data' => 0, }, ], } array1 = { 'type' => 'xfs', 'raid_level' => 'hybrid_xfs', 'mount_point' => '/data/fa', } node.default['fb_storage']['devices'] = [device1, device2] node.default['fb_storage']['arrays'] = [array1] node.automatic[attr_name]['by_device'] = { '/dev/sdb' => {}, '/dev/sdb1' => {}, '/dev/sdc' => {}, '/dev/sdc1' => {}, } node.automatic['mdadm'] = {} expect(FB::Storage).to receive(:build_mapping).and_return( { :disks => { '/dev/sdb' => device1, '/dev/sdc' => device2 }, :arrays => { '/dev/md0' => array1.merge( { 'members' => ['/dev/sdc1'], 'journal' => '/dev/sdb1' }, ), }, }, ) storage = FB::Storage.new(node) expect(storage.out_of_spec).to eq( { :mismatched_partitions => [], :mismatched_filesystems => [], :mismatched_arrays => [], :missing_partitions => [], :missing_filesystems => ['/dev/md0', '/dev/sdb1'], :missing_arrays => [], :incomplete_arrays => {}, :extra_arrays => [], }, ) end end context '#all_storage' do it 'should return all storage with one device' do node.default['fb_storage']['devices'] = [ single_partition_device, ] node.automatic[attr_name]['by_device'] = { '/dev/sdb' => {}, } expect(FB::Storage).to receive(:build_mapping).and_return( { :disks => { '/dev/sdb' => single_partition_device }, :arrays => {} }, ) storage = FB::Storage.new(node) expect(storage.all_storage).to eq( { :devices => ['/dev/sdb'], :partitions => ['/dev/sdb1'], :arrays => [], }, ) end it 'should return all storage with many devices' do node.default['fb_storage']['devices'] = [ single_partition_device, double_partition_device ] node.automatic[attr_name]['by_device'] = { '/dev/sdb' => {}, '/dev/fioa' => {}, } expect(FB::Storage).to receive(:build_mapping).and_return( { :disks => { '/dev/sdb' => single_partition_device, '/dev/fioa' => double_partition_device, }, :arrays => {}, }, ) storage = FB::Storage.new(node) expect(storage.all_storage).to eq( { :devices => ['/dev/sdb', '/dev/fioa'], :partitions => ['/dev/sdb1', '/dev/fioa1', '/dev/fioa2'], :arrays => [], }, ) end it 'should return all storage with arrays' do node.default['fb_storage']['devices'] = [ array_member, array_member ] node.default['fb_storage']['arrays'] = [single_array] node.automatic[attr_name]['by_device'] = { '/dev/sdb' => {}, '/dev/fioa' => {}, '/dev/md0' => { 'fs_type' => 'xfs' }, } node.automatic['mdadm']['md0'] = { 'level' => 1, 'members' => ['sdb1', 'fioa1'], } expect(FB::Storage).to receive(:build_mapping).and_return( { :disks => { '/dev/sdb' => array_member, '/dev/fioa' => array_member, }, :arrays => { '/dev/md0' => single_array }, }, ) storage = FB::Storage.new(node) expect(storage.all_storage).to eq( { :devices => ['/dev/sdb', '/dev/fioa'], :partitions => ['/dev/sdb1', '/dev/fioa1', '/dev/md0'], :arrays => ['/dev/md0'], }, ) end it 'should return all storage with arrays, skipping as necessary' do node.default['fb_storage']['devices'] = [ { '_skip' => true }, { '_skip' => true }, array_member, array_member ] node.default['fb_storage']['arrays'] = [ { '_skip' => true }, single_array, ] node.automatic[attr_name]['by_device'] = { '/dev/sdb' => {}, '/dev/sdc' => {}, '/dev/sdd' => {}, '/dev/sde' => {}, '/dev/md0' => { 'fs_type' => 'ext4' }, '/dev/md1' => { 'fs_type' => 'xfs' }, } node.automatic['mdadm']['md0'] = { 'level' => 1, 'members' => ['sdb1', 'sdc1'], } node.automatic['mdadm']['md1'] = { 'level' => 1, 'members' => ['sdd1', 'sde1'], } expect(FB::Storage).to receive(:build_mapping).and_return( { :disks => { '/dev/sdb' => { '_skip' => true }, '/dev/sdc' => { '_skip' => true }, '/dev/sdd' => array_member, '/dev/sde' => array_member, }, :arrays => { '/dev/md0' => { '_skip' => true }, '/dev/md1' => single_array, }, }, ) storage = FB::Storage.new(node) expect(storage.all_storage).to eq( { :devices => ['/dev/sdd', '/dev/sde'], :partitions => ['/dev/sdd1', '/dev/sde1', '/dev/md1'], :arrays => ['/dev/md1'], }, ) end end end context '#disks_from_automation' do it 'returns all disks from bar' do disks = ['sdb', 'fioa', '.', '..'] allow(File).to receive(:directory?).with( FB::Storage::REPLACED_DISKS_DIR, ).and_return(true) allow(Dir).to receive(:new).and_return(disks) expect(FB::Storage.disks_from_automation). to eq(['/dev/sdb', '/dev/fioa']) end end end