Merge pull request #36336 from pcuzner/ceph-volume-lsm

ceph-volume: add libstoragemgmt support
This commit is contained in:
pcuzner 2020-08-25 11:23:28 +12:00 committed by GitHub
commit 5fbfeaadba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 358 additions and 0 deletions

View File

@ -2,6 +2,8 @@
import pytest
from ceph_volume.util.device import Devices
from ceph_volume.util.lsmdisk import LSMDisk
import ceph_volume.util.lsmdisk as lsmdisk
@pytest.fixture
@ -59,6 +61,52 @@ def device_sys_api_keys(device_info):
report = Devices().json_report()[0]
return list(report['sys_api'].keys())
@pytest.fixture
def device_data(device_info):
device_info(
devices={
# example output of disk.get_devices()
'/dev/sdb': {
'human_readable_size': '1.82 TB',
'locked': 0,
'model': 'PERC H700',
'nr_requests': '128',
'partitions': {},
'path': '/dev/sdb',
'removable': '0',
'rev': '2.10',
'ro': '0',
'rotational': '1',
'sas_address': '',
'sas_device_handle': '',
'scheduler_mode': 'cfq',
'sectors': 0,
'sectorsize': '512',
'size': 1999844147200.0,
'support_discard': '',
'vendor': 'DELL',
}
}
)
dev = Devices().devices[0]
dev.lsm_data = {
"serialNum": 'S2X9NX0H935283',
"transport": 'SAS',
"mediaType": 'HDD',
"rpm": 10000,
"linkSpeed": 6000,
"health": 'Good',
"ledSupport": {
"IDENTsupport": 'Supported',
"IDENTstatus": 'Off',
"FAILsupport": 'Supported',
"FAILstatus": 'Off',
},
"errors": [],
}
return dev.json_report()
class TestInventory(object):
@ -69,6 +117,7 @@ class TestInventory(object):
'available',
'lvs',
'device_id',
'lsm_data',
]
expected_sys_api_keys = [
@ -92,6 +141,17 @@ class TestInventory(object):
'vendor',
]
expected_lsm_keys = [
'serialNum',
'transport',
'mediaType',
'rpm',
'linkSpeed',
'health',
'ledSupport',
'errors',
]
def test_json_inventory_keys_unexpected(self, device_report_keys):
for k in device_report_keys:
assert k in self.expected_keys, "unexpected key {} in report".format(k)
@ -108,3 +168,87 @@ class TestInventory(object):
for k in self.expected_sys_api_keys:
assert k in device_sys_api_keys, "expected key {} in sys_api field".format(k)
def test_lsm_data_type_unexpected(self, device_data):
assert isinstance(device_data['lsm_data'], dict), "lsm_data field must be of type dict"
def test_lsm_data_keys_unexpected(self, device_data):
for k in device_data['lsm_data'].keys():
assert k in self.expected_lsm_keys, "unexpected key {} in lsm_data field".format(k)
def test_lsm_data_keys_missing(self, device_data):
lsm_keys = device_data['lsm_data'].keys()
assert lsm_keys
for k in self.expected_lsm_keys:
assert k in lsm_keys, "expected key {} in lsm_data field".format(k)
@pytest.fixture
def lsm_info(monkeypatch):
def mock_query_lsm(_, func, path):
query_map = {
'serial_num_get': "S2X9NX0H935283",
'link_type_get': 6,
'rpm_get': 0,
'link_speed_get': 6000,
'health_status_get': 2,
'led_status_get': 36,
}
return query_map.get(func, 'Unknown')
# mocked states and settings taken from the libstoragemgmt code base
# c_binding/include/libstoragemgmt/libstoragemgmt_types.h at
# https://github.com/libstorage/libstoragemgmt/
mock_health_map = {
-1: "Unknown",
0: "Fail",
1: "Warn",
2: "Good",
}
mock_transport_map = {
-1: "Unavailable",
0: "Fibre Channel",
2: "IBM SSA",
3: "Serial Bus",
4: "SCSI RDMA",
5: "iSCSI",
6: "SAS",
7: "ADT (Tape)",
8: "ATA/SATA",
9: "USB",
10: "SCSI over PCI-E",
11: "PCI-E",
}
class MockLEDStates():
LED_STATUS_UNKNOWN = 1
LED_STATUS_IDENT_ON = 2
LED_STATUS_IDENT_OFF = 4
LED_STATUS_IDENT_UNKNOWN = 8
LED_STATUS_FAULT_ON = 16
LED_STATUS_FAULT_OFF = 32
LED_STATUS_FAULT_UNKNOWN = 64
monkeypatch.setattr(LSMDisk, '_query_lsm', mock_query_lsm)
monkeypatch.setattr(lsmdisk, 'health_map', mock_health_map)
monkeypatch.setattr(lsmdisk, 'transport_map', mock_transport_map)
monkeypatch.setattr(lsmdisk, 'lsm_Disk', MockLEDStates)
return LSMDisk('/dev/sda')
class TestLSM(object):
def test_lsmdisk_health(self, lsm_info):
assert lsm_info.health == "Good"
def test_lsmdisk_transport(self, lsm_info):
assert lsm_info.transport == 'SAS'
def test_lsmdisk_mediatype(self, lsm_info):
assert lsm_info.media_type == 'Flash'
def test_lsmdisk_led_ident_support(self, lsm_info):
assert lsm_info.led_ident_support == 'Supported'
def test_lsmdisk_led_ident(self, lsm_info):
assert lsm_info.led_ident_state == 'Off'
def test_lsmdisk_led_fault_support(self, lsm_info):
assert lsm_info.led_fault_support == 'Supported'
def test_lsmdisk_led_fault(self, lsm_info):
assert lsm_info.led_fault_state == 'Off'
def test_lsmdisk_report(self, lsm_info):
assert isinstance(lsm_info.json_report(), dict)

View File

@ -5,6 +5,7 @@ from functools import total_ordering
from ceph_volume import sys_info, process
from ceph_volume.api import lvm
from ceph_volume.util import disk
from ceph_volume.util.lsmdisk import LSMDisk
from ceph_volume.util.constants import ceph_disk_guids
report_template = """
@ -63,6 +64,7 @@ class Device(object):
'path',
'sys_api',
'device_id',
'lsm_data',
]
pretty_report_sys_fields = [
'human_readable_size',
@ -90,6 +92,7 @@ class Device(object):
self._exists = None
self._is_lvm_member = None
self._parse()
self.lsm_data = self.fetch_lsm()
self.available_lvm, self.rejected_reasons_lvm = self._check_lvm_reject_reasons()
self.available_raw, self.rejected_reasons_raw = self._check_raw_reject_reasons()
@ -99,6 +102,21 @@ class Device(object):
self.device_id = self._get_device_id()
def fetch_lsm(self):
'''
Attempt to fetch libstoragemgmt (LSM) metadata, and return to the caller
as a dict. An empty dict is passed back to the caller if the target path
is not a block device, or lsm is unavailable on the host. Otherwise the
json returned will provide LSM attributes, and any associated errors that
lsm encountered when probing the device.
'''
if not self.exists or not self.is_device:
return {}
lsm_disk = LSMDisk(self.path)
return lsm_disk.json_report()
def __lt__(self, other):
'''
Implementing this method and __eq__ allows the @total_ordering

View File

@ -0,0 +1,196 @@
"""
This module handles the interaction with libstoragemgmt for local disk
devices. Interaction may fail with LSM for a number of issues, but the
intent here is to make this a soft fail, since LSM related data is not
a critical component of ceph-volume.
"""
import logging
try:
from lsm import LocalDisk, LsmError
from lsm import Disk as lsm_Disk
except ImportError:
lsm_available = False
transport_map = {}
health_map = {}
lsm_Disk = None
else:
lsm_available = True
transport_map = {
lsm_Disk.LINK_TYPE_UNKNOWN: "Unavailable",
lsm_Disk.LINK_TYPE_FC: "Fibre Channel",
lsm_Disk.LINK_TYPE_SSA: "IBM SSA",
lsm_Disk.LINK_TYPE_SBP: "Serial Bus",
lsm_Disk.LINK_TYPE_SRP: "SCSI RDMA",
lsm_Disk.LINK_TYPE_ISCSI: "iSCSI",
lsm_Disk.LINK_TYPE_SAS: "SAS",
lsm_Disk.LINK_TYPE_ADT: "ADT (Tape)",
lsm_Disk.LINK_TYPE_ATA: "ATA/SATA",
lsm_Disk.LINK_TYPE_USB: "USB",
lsm_Disk.LINK_TYPE_SOP: "SCSI over PCI-E",
lsm_Disk.LINK_TYPE_PCIE: "PCI-E",
}
health_map = {
lsm_Disk.HEALTH_STATUS_UNKNOWN: "Unknown",
lsm_Disk.HEALTH_STATUS_FAIL: "Fail",
lsm_Disk.HEALTH_STATUS_WARN: "Warn",
lsm_Disk.HEALTH_STATUS_GOOD: "Good",
}
logger = logging.getLogger(__name__)
class LSMDisk:
def __init__(self, dev_path):
self.dev_path = dev_path
self.error_list = set()
if lsm_available:
self.lsm_available = True
self.disk = LocalDisk()
else:
self.lsm_available = False
self.error_list.add("libstoragemgmt (lsm module) is unavailable")
logger.info("LSM information is unavailable: libstoragemgmt is not installed")
self.disk = None
self.led_bits = None
@property
def errors(self):
"""show any errors that the LSM interaction has encountered (str)"""
return ", ".join(self.error_list)
def _query_lsm(self, func, path):
"""Common method used to call the LSM functions, returning the function's result or None"""
# if disk is None, lsm is unavailable so all calls should return None
if self.disk is None:
return None
method = getattr(self.disk, func)
try:
output = method(path)
except LsmError as err:
logger.error("LSM Error: {}".format(err._msg))
self.error_list.add(err._msg)
return None
else:
return output
@property
def led_status(self):
"""Fetch LED status, store in the LSMDisk object and return current status (int)"""
if self.led_bits is None:
self.led_bits = self._query_lsm('led_status_get', self.dev_path) or 1
return self.led_bits
else:
return self.led_bits
@property
def led_ident_state(self):
"""Query a disks IDENT LED state to discover when it is On, Off or Unknown (str)"""
if self.led_status == 1:
return "Unsupported"
if self.led_status & lsm_Disk.LED_STATUS_IDENT_ON == lsm_Disk.LED_STATUS_IDENT_ON:
return "On"
elif self.led_status & lsm_Disk.LED_STATUS_IDENT_OFF == lsm_Disk.LED_STATUS_IDENT_OFF:
return "Off"
elif self.led_status & lsm_Disk.LED_STATUS_IDENT_UNKNOWN == lsm_Disk.LED_STATUS_IDENT_UNKNOWN:
return "Unknown"
return "Unsupported"
@property
def led_fault_state(self):
"""Query a disks FAULT LED state to discover when it is On, Off or Unknown (str)"""
if self.led_status == 1:
return "Unsupported"
if self.led_status & lsm_Disk.LED_STATUS_FAULT_ON == lsm_Disk.LED_STATUS_FAULT_ON:
return "On"
elif self.led_status & lsm_Disk.LED_STATUS_FAULT_OFF == lsm_Disk.LED_STATUS_FAULT_OFF:
return "Off"
elif self.led_status & lsm_Disk.LED_STATUS_FAULT_UNKNOWN == lsm_Disk.LED_STATUS_FAULT_UNKNOWN:
return "Unknown"
return "Unsupported"
@property
def led_ident_support(self):
"""Query the LED state to determine IDENT support: Unknown, Supported, Unsupported (str)"""
if self.led_status == 1:
return "Unknown"
ident_states = (
lsm_Disk.LED_STATUS_IDENT_ON +
lsm_Disk.LED_STATUS_IDENT_OFF +
lsm_Disk.LED_STATUS_IDENT_UNKNOWN
)
if (self.led_status & ident_states) == 0:
return "Unsupported"
return "Supported"
@property
def led_fault_support(self):
"""Query the LED state to determine FAULT support: Unknown, Supported, Unsupported (str)"""
if self.led_status == 1:
return "Unknown"
fail_states = (
lsm_Disk.LED_STATUS_FAULT_ON +
lsm_Disk.LED_STATUS_FAULT_OFF +
lsm_Disk.LED_STATUS_FAULT_UNKNOWN
)
if self.led_status & fail_states == 0:
return "Unsupported"
return "Supported"
@property
def health(self):
"""Determine the health of the disk from LSM : Unknown, Fail, Warn or Good (str)"""
_health_int = self._query_lsm('health_status_get', self.dev_path)
return health_map.get(_health_int, "Unknown")
@property
def transport(self):
"""Translate a disks link type to a human readable format (str)"""
_link_type = self._query_lsm('link_type_get', self.dev_path)
return transport_map.get(_link_type, "Unknown")
@property
def media_type(self):
"""Use the rpm value to determine the type of disk media: Flash or HDD (str)"""
_rpm = self._query_lsm('rpm_get', self.dev_path)
if _rpm is not None:
if _rpm == 0:
return "Flash"
elif _rpm > 1:
return "HDD"
return "Unknown"
def json_report(self):
"""Return the LSM related metadata for the current local disk (dict)"""
if self.lsm_available:
return {
"serialNum": self._query_lsm('serial_num_get', self.dev_path) or "Unknown",
"transport": self.transport,
"mediaType": self.media_type,
"rpm": self._query_lsm('rpm_get', self.dev_path) or "Unknown",
"linkSpeed": self._query_lsm('link_speed_get', self.dev_path) or "Unknown",
"health": self.health,
"ledSupport": {
"IDENTsupport": self.led_ident_support,
"IDENTstatus": self.led_ident_state,
"FAILsupport": self.led_fault_support,
"FAILstatus": self.led_fault_state,
},
"errors": list(self.error_list)
}
else:
return {}