1e8f38bbb8
list_block_devices should report all devices that it can find. If an error occurs during block device analysing, then it should be silently ignored and the device should be skipped. This change will let provisioning to proceed if this skipped device is not required for provisioning purposes. Change-Id: I9399b1e0855493289e82f40ffd8c892f89412f1c Related-Bug: #1486601
474 lines
18 KiB
Python
474 lines
18 KiB
Python
# Copyright 2014 Mirantis, Inc.
|
|
#
|
|
# 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.
|
|
|
|
import mock
|
|
import unittest2
|
|
|
|
from fuel_agent import errors
|
|
from fuel_agent.utils import hardware as hu
|
|
from fuel_agent.utils import utils
|
|
|
|
|
|
class TestHardwareUtils(unittest2.TestCase):
|
|
|
|
@mock.patch.object(utils, 'execute')
|
|
def test_parse_dmidecode(self, exec_mock):
|
|
exec_mock.return_value = ["""
|
|
System Slot Information
|
|
Designation: PCIEX16_1
|
|
ID: 1
|
|
Bus Address: 0000:00:01.0
|
|
Characteristics:
|
|
3.3 V is provided
|
|
PME signal is supported
|
|
|
|
System Slot Information
|
|
Type: 32-bit PCI Express
|
|
ID: 3
|
|
Characteristics:
|
|
Opening is shared
|
|
Bus Address: 0000:00:1c.4
|
|
|
|
|
|
"""]
|
|
|
|
expected = [{"designation": "PCIEX16_1",
|
|
"id": "1",
|
|
"characteristics": ["3.3 V is provided",
|
|
"PME signal is supported"],
|
|
"bus address": "0000:00:01.0"},
|
|
{"type": "32-bit PCI Express",
|
|
"id": "3",
|
|
"characteristics": ["Opening is shared"],
|
|
"bus address": "0000:00:1c.4"}]
|
|
|
|
self.assertEqual(expected, hu.parse_dmidecode("fake_type"))
|
|
exec_mock.assert_called_once_with("dmidecode", "-q", "--type",
|
|
"fake_type")
|
|
|
|
@mock.patch.object(utils, 'execute')
|
|
def test_parse_lspci(self, exec_mock):
|
|
exec_mock.return_value = ["""Slot: 07:00.0
|
|
Class: PCI bridge
|
|
Vendor: ASMedia Technology Inc.
|
|
Device: ASM1083/1085 PCIe to PCI Bridge
|
|
Rev: 01
|
|
ProgIf: 01
|
|
|
|
Slot: 09:00.0
|
|
Class: IDE interface
|
|
Vendor: Marvell Technology Group Ltd.
|
|
Device: 88SE6121 SATA II / PATA Controller
|
|
SVendor: ASUSTeK Computer Inc.
|
|
SDevice: Device 82a2
|
|
Rev: b2
|
|
ProgIf: 8f
|
|
|
|
"""]
|
|
|
|
expected = [{'class': 'PCI bridge',
|
|
'device': 'ASM1083/1085 PCIe to PCI Bridge',
|
|
'progif': '01',
|
|
'rev': '01',
|
|
'slot': '07:00.0',
|
|
'vendor': 'ASMedia Technology Inc.'},
|
|
{'class': 'IDE interface',
|
|
'device': '88SE6121 SATA II / PATA Controller',
|
|
'progif': '8f',
|
|
'rev': 'b2',
|
|
'sdevice': 'Device 82a2',
|
|
'slot': '09:00.0',
|
|
'svendor': 'ASUSTeK Computer Inc.',
|
|
'vendor': 'Marvell Technology Group Ltd.'}]
|
|
|
|
self.assertEqual(expected, hu.parse_lspci())
|
|
exec_mock.assert_called_once_with('lspci', '-vmm', '-D')
|
|
|
|
@mock.patch.object(utils, 'execute')
|
|
def test_parse_simple_kv(self, exec_mock):
|
|
exec_mock.return_value = ["""driver: r8169
|
|
version: 2.3LK-NAPI
|
|
firmware-version: rtl_nic/rtl8168e-2.fw
|
|
bus-info: 0000:06:00.0
|
|
supports-statistics: yes
|
|
supports-test: no
|
|
supports-eeprom-access: no
|
|
supports-register-dump: yes
|
|
|
|
"""]
|
|
|
|
expected = {'driver': 'r8169',
|
|
'version': '2.3LK-NAPI',
|
|
'firmware-version': 'rtl_nic/rtl8168e-2.fw',
|
|
'bus-info': '0000:06:00.0',
|
|
'supports-statistics': 'yes',
|
|
'supports-test': 'no',
|
|
'supports-eeprom-access': 'no',
|
|
'supports-register-dump': 'yes'}
|
|
|
|
self.assertEqual(expected, hu.parse_simple_kv('fake', 'cmd'))
|
|
exec_mock.assert_called_once_with('fake', 'cmd')
|
|
|
|
@mock.patch.object(utils, 'execute')
|
|
def test_udevreport(self, mock_exec):
|
|
# should run udevadm info OS command
|
|
# in order to get udev properties for a device
|
|
mock_exec.return_value = (
|
|
'DEVLINKS=\'/dev/disk/by-id/fakeid1 /dev/disk/by-id/fakeid2\'\n'
|
|
'DEVNAME=\'/dev/fake\'\n'
|
|
'DEVPATH=\'/devices/fakepath\'\n'
|
|
'DEVTYPE=\'disk\'\n'
|
|
'MAJOR=\'11\'\n'
|
|
'MINOR=\'0\'\n'
|
|
'ID_BUS=\'fakebus\'\n'
|
|
'ID_MODEL=\'fakemodel\'\n'
|
|
'ID_SERIAL_SHORT=\'fakeserial\'\n'
|
|
'ID_WWN=\'fakewwn\'\n'
|
|
'ID_CDROM=\'1\'\n'
|
|
'ANOTHER=\'another\'\n',
|
|
''
|
|
)
|
|
expected = {
|
|
'DEVLINKS': ['/dev/disk/by-id/fakeid1', '/dev/disk/by-id/fakeid2'],
|
|
'DEVNAME': '/dev/fake',
|
|
'DEVPATH': '/devices/fakepath',
|
|
'DEVTYPE': 'disk',
|
|
'MAJOR': '11',
|
|
'MINOR': '0',
|
|
'ID_BUS': 'fakebus',
|
|
'ID_MODEL': 'fakemodel',
|
|
'ID_SERIAL_SHORT': 'fakeserial',
|
|
'ID_WWN': 'fakewwn',
|
|
'ID_CDROM': '1'
|
|
}
|
|
self.assertEqual(expected, hu.udevreport('/dev/fake'))
|
|
mock_exec.assert_called_once_with('udevadm',
|
|
'info',
|
|
'--query=property',
|
|
'--export',
|
|
'--name=/dev/fake',
|
|
check_exit_code=[0])
|
|
|
|
@mock.patch.object(utils, 'execute')
|
|
def test_blockdevreport(self, mock_exec):
|
|
# should run blockdev OS command
|
|
# in order to get block device properties
|
|
cmd = ['blockdev', '--getsz', '--getro', '--getss', '--getpbsz',
|
|
'--getsize64', '--getiomin', '--getioopt', '--getra',
|
|
'--getalignoff', '--getmaxsect', '/dev/fake']
|
|
mock_exec.return_value = (
|
|
'625142448\n0\n512\n4096\n320072933376\n4096\n0\n256\n0\n1024',
|
|
''
|
|
)
|
|
expected = {
|
|
'sz': '625142448',
|
|
'ro': '0',
|
|
'ss': '512',
|
|
'pbsz': '4096',
|
|
'size64': '320072933376',
|
|
'iomin': '4096',
|
|
'ioopt': '0',
|
|
'ra': '256',
|
|
'alignoff': '0',
|
|
'maxsect': '1024'
|
|
}
|
|
self.assertEqual(expected, hu.blockdevreport('/dev/fake'))
|
|
mock_exec.assert_called_once_with(*cmd, check_exit_code=[0])
|
|
|
|
@mock.patch('six.moves.builtins.open')
|
|
def test_extrareport(self, mock_open):
|
|
# should read some files from sysfs e.g. /sys/block/fake/removable
|
|
# in order to get some device properties
|
|
def with_side_effect(arg):
|
|
mock_with = mock.MagicMock()
|
|
mock_with.__exit__.return_value = None
|
|
mock_file = mock.Mock()
|
|
if arg == '/sys/block/fake/removable':
|
|
mock_file.read.return_value = '0\n'
|
|
elif arg == '/sys/block/fake/device/state':
|
|
mock_file.read.return_value = 'running\n'
|
|
elif arg == '/sys/block/fake/device/timeout':
|
|
mock_file.read.return_value = '30\n'
|
|
elif arg == '/sys/block/fake/device/vendor':
|
|
mock_file.read.return_value = 'VENDORNAME \n'
|
|
mock_with.__enter__.return_value = mock_file
|
|
return mock_with
|
|
mock_open.side_effect = with_side_effect
|
|
expected = {'removable': '0', 'state': 'running', 'timeout': '30',
|
|
'vendor': 'VENDORNAME'}
|
|
self.assertEqual(expected, hu.extrareport('/dev/fake'))
|
|
|
|
@mock.patch('six.moves.builtins.open')
|
|
def test_extrareport_exceptions(self, mock_open):
|
|
mock_open.side_effect = Exception('foo')
|
|
expected = {}
|
|
self.assertEqual(expected, hu.extrareport('/dev/fake'))
|
|
|
|
@mock.patch.object(hu, 'blockdevreport')
|
|
@mock.patch.object(hu, 'udevreport')
|
|
def test_is_disk_uspec_bspec_none(self, mock_ureport, mock_breport):
|
|
# should call udevreport if uspec is None
|
|
# should call blockdevreport if bspec is None
|
|
# should return True if uspec and bspec are empty
|
|
mock_ureport.return_value = {}
|
|
mock_breport.return_value = {}
|
|
self.assertTrue(hu.is_disk('/dev/fake'))
|
|
mock_ureport.assert_called_once_with('/dev/fake')
|
|
mock_breport.assert_called_once_with('/dev/fake')
|
|
|
|
@mock.patch.object(hu, 'udevreport')
|
|
def test_is_disk_uspec_none(self, mock_ureport):
|
|
# should call udevreport if uspec is None but bspec is not None
|
|
bspec = {'key': 'value'}
|
|
mock_ureport.return_value = {}
|
|
hu.is_disk('/dev/fake', bspec=bspec)
|
|
mock_ureport.assert_called_once_with('/dev/fake')
|
|
|
|
@mock.patch.object(hu, 'blockdevreport')
|
|
def test_is_disk_bspec_none(self, mock_breport):
|
|
# should call blockdevreport if bspec is None but uspec is not None
|
|
uspec = {'key': 'value'}
|
|
mock_breport.return_value = {}
|
|
hu.is_disk('/dev/fake', uspec=uspec)
|
|
mock_breport.assert_called_once_with('/dev/fake')
|
|
|
|
@mock.patch.object(hu, 'blockdevreport')
|
|
def test_is_disk_cdrom(self, mock_breport):
|
|
# should return False if udev ID_CDROM is set to 1
|
|
mock_breport.return_value = {}
|
|
uspec = {
|
|
'ID_CDROM': '1'
|
|
}
|
|
self.assertFalse(hu.is_disk('/dev/fake', uspec=uspec))
|
|
|
|
@mock.patch.object(hu, 'blockdevreport')
|
|
def test_is_disk_partition(self, mock_breport):
|
|
# should return False if udev DEVTYPE is partition
|
|
mock_breport.return_value = {}
|
|
uspec = {
|
|
'DEVTYPE': 'partition'
|
|
}
|
|
self.assertFalse(hu.is_disk('/dev/fake', uspec=uspec))
|
|
|
|
@mock.patch.object(hu, 'blockdevreport')
|
|
def test_is_disk_major(self, mock_breport):
|
|
# should return False if udev MAJOR is not in a list of
|
|
# major numbers which are used for disks
|
|
# look at kernel/Documentation/devices.txt
|
|
mock_breport.return_value = {}
|
|
valid_majors = [3, 8, 65, 66, 67, 68, 69, 70, 71, 104, 105,
|
|
106, 107, 108, 109, 110, 111, 202, 252, 253, 259]
|
|
for major in (set(range(1, 261)) - set(valid_majors)):
|
|
uspec = {
|
|
'MAJOR': str(major)
|
|
}
|
|
self.assertFalse(hu.is_disk('/dev/fake', uspec=uspec))
|
|
|
|
@mock.patch.object(hu, 'udevreport')
|
|
def test_is_disk_readonly(self, mock_ureport):
|
|
# should return False if device is read only
|
|
mock_ureport.return_value = {}
|
|
bspec = {
|
|
'ro': '1'
|
|
}
|
|
self.assertFalse(hu.is_disk('/dev/fake', bspec=bspec))
|
|
|
|
@mock.patch('fuel_agent.utils.hardware.utils.execute')
|
|
def test_get_block_devices_from_udev_db(self, mock_exec):
|
|
mock_exec.return_value = ("""P: /devices/virtual/block/loop0
|
|
N: loop0
|
|
E: DEVNAME=/dev/loop0
|
|
E: DEVPATH=/devices/virtual/block/loop0
|
|
E: DEVTYPE=disk
|
|
E: MAJOR=7
|
|
E: SUBSYSTEM=block
|
|
|
|
P: /devices/pci0000:00/0000:00:1f.2/ata1/host0/target0:0:0/0:0:0:0/block/sda
|
|
N: sda
|
|
S: disk/by-id/wwn-0x5000c5004008ac0f
|
|
S: disk/by-path/pci-0000:00:1f.2-scsi-0:0:0:0
|
|
E: DEVNAME=/dev/sda
|
|
E: DEVTYPE=disk
|
|
E: ID_ATA=1
|
|
E: MAJOR=8
|
|
E: SUBSYSTEM=block
|
|
E: UDEV_LOG=3
|
|
|
|
P: /devices/pci:00/:00:04.0/misc/nvme0
|
|
N: nvme0
|
|
E: DEVNAME=/dev/nvme0
|
|
E: DEVPATH=/devices/pci:00/:00:04.0/misc/nvme0
|
|
E: MAJOR=10
|
|
E: MINOR=57
|
|
E: SUBSYSTEM=misc
|
|
|
|
P: /devices/pci:00/:00:04.0/block/nvme0n1
|
|
N: nvme0n1
|
|
E: DEVNAME=/dev/nvme0n1
|
|
E: DEVPATH=/devices/pci:00/:00:04.0/block/nvme0n1
|
|
E: DEVTYPE=disk
|
|
E: MAJOR=259
|
|
E: MINOR=0
|
|
E: SUBSYSTEM=block
|
|
E: USEC_INITIALIZED=87744
|
|
|
|
P: /devices/pci0000:00/0000:00:1c.1/target16:0:0/16:0:0:0/block/sr0
|
|
E: DEVTYPE=disk
|
|
E: DEVNAME=/dev/sr0
|
|
E: MAJOR=11
|
|
E: MINOR=0
|
|
E: SEQNUM=4400
|
|
E: SUBSYSTEM=block
|
|
|
|
P: /devices/pci0000:00/0000:00:1f.2/ata1/host0/target0:0:0/0:0:0:0/block/sda
|
|
N: sda
|
|
S: disk/by-id/wwn-0x5000c5004008ac0f
|
|
S: disk/by-path/pci-0000:00:1f.2-scsi-0:0:0:0
|
|
E: DEVNAME=/dev/sda1
|
|
E: DEVTYPE=partition
|
|
E: ID_ATA=1
|
|
E: SUBSYSTEM=block
|
|
E: MAJOR=8
|
|
E: UDEV_LOG=3""", '')
|
|
|
|
self.assertEqual(['/dev/sda', '/dev/nvme0n1', '/dev/sda1'],
|
|
hu.get_block_devices_from_udev_db())
|
|
|
|
@mock.patch.object(hu, 'get_block_devices_from_udev_db')
|
|
@mock.patch.object(hu, 'is_disk')
|
|
@mock.patch.object(hu, 'extrareport')
|
|
@mock.patch.object(hu, 'blockdevreport')
|
|
@mock.patch.object(hu, 'udevreport')
|
|
def test_list_block_devices(self, mock_ureport, mock_breport, mock_ereport,
|
|
mock_isdisk, mock_get_devs):
|
|
# should run blockdev --report command
|
|
# in order to get a list of block devices
|
|
# should call report methods to get device info
|
|
# should call is_disk method to filter out
|
|
# those block devices which are not disks
|
|
mock_get_devs.return_value = ['/dev/fake', '/dev/fake1', '/dev/sr0']
|
|
|
|
def isdisk_side_effect(arg, uspec=None, bspec=None):
|
|
if arg == '/dev/fake':
|
|
return True
|
|
elif arg in ('/dev/fake1', '/dev/sr0'):
|
|
return False
|
|
mock_isdisk.side_effect = isdisk_side_effect
|
|
mock_ureport.return_value = {'key0': 'value0'}
|
|
mock_breport.return_value = {'key1': 'value1'}
|
|
mock_ereport.return_value = {'key2': 'value2'}
|
|
|
|
expected = [{
|
|
'device': '/dev/fake',
|
|
'uspec': {'key0': 'value0'},
|
|
'bspec': {'key1': 'value1'},
|
|
'espec': {'key2': 'value2'}
|
|
}]
|
|
self.assertEqual(hu.list_block_devices(), expected)
|
|
self.assertEqual(mock_ureport.call_args_list, [mock.call('/dev/fake'),
|
|
mock.call('/dev/fake1'), mock.call('/dev/sr0')])
|
|
self.assertEqual(mock_breport.call_args_list, [mock.call('/dev/fake'),
|
|
mock.call('/dev/fake1'), mock.call('/dev/sr0')])
|
|
self.assertEqual(mock_ereport.call_args_list, [mock.call('/dev/fake'),
|
|
mock.call('/dev/fake1'), mock.call('/dev/sr0')])
|
|
|
|
@mock.patch.object(hu, 'get_block_devices_from_udev_db')
|
|
@mock.patch.object(hu, 'is_disk')
|
|
@mock.patch.object(hu, 'extrareport')
|
|
@mock.patch.object(hu, 'blockdevreport')
|
|
@mock.patch.object(hu, 'udevreport')
|
|
def test_list_block_devices_skip_block_device(self, mock_ureport,
|
|
mock_breport, mock_ereport,
|
|
mock_isdisk, mock_get_devs):
|
|
mock_get_devs.return_value = ['/dev/disk',
|
|
'/dev/wrong_disk_skipped',
|
|
'/dev/wrong_disk_skipped_too']
|
|
mock_isdisk.return_value = True
|
|
mock_ureport.return_value = {}
|
|
mock_ereport.side_effect = [
|
|
{'removable': '1'},
|
|
errors.ProcessExecutionError,
|
|
KeyError,
|
|
]
|
|
mock_breport.return_value = {'key1': 'value1'}
|
|
expected = [{
|
|
'device': '/dev/disk',
|
|
'uspec': {},
|
|
'bspec': {'key1': 'value1'},
|
|
'espec': {'removable': '1'},
|
|
}]
|
|
self.assertEqual(hu.list_block_devices(), expected)
|
|
expected_calls = [mock.call('/dev/disk'),
|
|
mock.call('/dev/wrong_disk_skipped'),
|
|
mock.call('/dev/wrong_disk_skipped_too')]
|
|
self.assertEqual(expected_calls, mock_ureport.call_args_list)
|
|
mock_breport.assert_called_once_with('/dev/disk')
|
|
self.assertEqual(expected_calls, mock_ereport.call_args_list)
|
|
|
|
def test_match_device_devlinks(self):
|
|
# should return true if at least one by-id link from first uspec
|
|
# matches by-id link from another uspec
|
|
uspec1 = {'DEVLINKS': ['/dev/disk/by-path/fakepath',
|
|
'/dev/disk/by-id/fakeid1',
|
|
'/dev/disk/by-id/fakeid2']}
|
|
uspec2 = {'DEVLINKS': ['/dev/disk/by-id/fakeid2',
|
|
'/dev/disk/by-id/fakeid3']}
|
|
self.assertTrue(hu.match_device(uspec1, uspec2))
|
|
|
|
def test_match_device_wwn(self):
|
|
# should return true if ID_WWN is given
|
|
# and if it is the same in both uspecs
|
|
# and if DEVTYPE is given and if DEVTYPE is disk
|
|
# or if DEVTYPE is partition and MINOR is the same for both uspecs
|
|
uspec1 = uspec2 = {'ID_WWN': 'fakewwn',
|
|
'DEVTYPE': 'disk'}
|
|
self.assertTrue(hu.match_device(uspec1, uspec2))
|
|
uspec1 = uspec2 = {'ID_WWN': 'fakewwn',
|
|
'DEVTYPE': 'partition',
|
|
'MINOR': '1'}
|
|
self.assertTrue(hu.match_device(uspec1, uspec2))
|
|
|
|
def test_match_device_wwn_false(self):
|
|
# should return false if ID_WWN is given
|
|
# and does not match each other
|
|
uspec1 = {'ID_WWN': 'fakewwn1'}
|
|
uspec2 = {'ID_WWN': 'fakewwn2'}
|
|
self.assertFalse(hu.match_device(uspec1, uspec2))
|
|
|
|
def test_match_device_devpath(self):
|
|
# should return true if DEVPATH is given
|
|
# and if it is the same for both uspecs
|
|
uspec1 = uspec2 = {'DEVPATH': '/devices/fake'}
|
|
self.assertTrue(hu.match_device(uspec1, uspec2))
|
|
|
|
def test_match_device_serial(self):
|
|
# should return true if ID_SERIAL_SHORT is given
|
|
# and if it is the same for both uspecs
|
|
# and if DEVTYPE is given and if it is 'disk'
|
|
uspec1 = uspec2 = {'ID_SERIAL_SHORT': 'fakeserial',
|
|
'DEVTYPE': 'disk'}
|
|
self.assertTrue(hu.match_device(uspec1, uspec2))
|
|
|
|
def test_match_device_serial_false(self):
|
|
# should return false if ID_SERIAL_SHORT is given
|
|
# and if it does not match each other
|
|
uspec1 = {'ID_SERIAL_SHORT': 'fakeserial1'}
|
|
uspec2 = {'ID_SERIAL_SHORT': 'fakeserial2'}
|
|
self.assertFalse(hu.match_device(uspec1, uspec2))
|
|
|
|
def test_match_device_false(self):
|
|
uspec1 = {'ID_WWN': 'fakewwn1', 'DEVTYPE': 'disk'}
|
|
uspec2 = {'ID_WWN': 'fakewwn1', 'DEVTYPE': 'partition'}
|
|
self.assertFalse(hu.match_device(uspec1, uspec2))
|