mirror of
https://github.com/ceph/ceph
synced 2025-02-23 11:07:35 +00:00
Merge pull request #36336 from pcuzner/ceph-volume-lsm
ceph-volume: add libstoragemgmt support
This commit is contained in:
commit
5fbfeaadba
@ -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)
|
@ -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
|
||||
|
196
src/ceph-volume/ceph_volume/util/lsmdisk.py
Normal file
196
src/ceph-volume/ceph_volume/util/lsmdisk.py
Normal 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 {}
|
Loading…
Reference in New Issue
Block a user