sysinv: Add support for mpath device

The device node in /dev/ and device path in /dev/disk/by-path
can not be used directly for mpath devices, use /dev/mapper/mpathN
and /dev/disk/by-id/dm-uuid-mpath-<WWID> instead, and change
the following accordingly:

* No DB schema change, the column used for by-path will be used
  for /dev/disk/by-id/dm-uuid-mpath-<WWID>

* common/utils: add several common functions for the device_node
  and device_path mapping.

Test Plan:

PASS: AIO-SX without Ceph
PASS: AIO-SX with Ceph, 1 osd
PASS: AIO-SX with Ceph, 2 osd
PASS: AIO-SX with Ceph, 4 osd
PASS: Add/Remove partition
PASS: Extend physical volumes with partition

Story: 2010046
Task: 45429

Signed-off-by: Jackie Huang <jackie.huang@windriver.com>
Signed-off-by: Thiago Miranda <ThiagoOliveira.Miranda@windriver.com>
Change-Id: I075a6b64118ba05852c73f80589e0c87dfaa7017
This commit is contained in:
Jackie Huang 2022-03-04 02:00:17 -05:00 committed by Felipe Sanches Zanoni
parent adb7b41308
commit 8c6cee5639
12 changed files with 179 additions and 46 deletions

View File

@ -169,8 +169,17 @@ def _get_disk_device_path(part_device_path):
:param part_device_path: the partition's device path
:returns the device path of the disk on which the partition resides
"""
return re.match('(/dev/disk/by-path/(.+))-part([0-9]+)',
part_device_path).group(1)
disk_device_path = ""
if 'by-path' in part_device_path:
disk_device_path = re.match('(/dev/disk/by-path/(.+))-part([0-9]+)',
part_device_path).group(1)
if constants.DEVICE_NAME_MPATH in part_device_path:
match_path = re.match('(/dev/disk/by-id/.+)-part([0-9]+)(-mpath.*)',
part_device_path)
if match_path:
disk_device_path = match_path.group(1) + match_path.group(3)
return disk_device_path
def _get_partition_number(part_device_path):
@ -178,7 +187,7 @@ def _get_partition_number(part_device_path):
:param part_device_path: the partition's device path
:returns the partition's number
"""
return re.match('.*?([0-9]+)$', part_device_path).group(1)
return utils.get_part_number(part_device_path)
def _partition_exists(part_device_path):
@ -335,7 +344,8 @@ def _create_partition(disk_device_path, part_number, start_mib, size_mib,
# After a partition is created we have to wait for udev to create the
# corresponding device node. Otherwise if we try to open it will fail.
part_device_path = '{}-part{}'.format(disk_device_path, part_number)
part_device_path = utils.get_part_device_path(disk_device_path,
part_number)
_wait_for_partition(part_device_path,
loop_wait_time=PARTITION_LOOP_WAIT_TIME)
@ -524,8 +534,8 @@ def create_partitions(data, mode, pfile):
new_partition = _create_partition(
disk_device_path, partition_number, start_mib,
size_mib, type_code)
part_device_path = '{}-part{}'.format(disk_device_path,
partition_number)
part_device_path = utils.get_part_device_path(disk_device_path,
partition_number)
output, _, _ = _command(["udevadm", "settle", "-E",
disk_device_path])

View File

@ -89,6 +89,8 @@ class DiskOperator(object):
if device is not None:
if constants.DEVICE_NAME_NVME in device:
re_line = re.compile(r'^(nvme[0-9]*n[0-9]*)')
elif constants.DEVICE_NAME_DM in device:
return utils.get_mpath_from_dm(os.path.join("/dev", device))
else:
re_line = re.compile(r'^(\D*)')
match = re_line.search(device)
@ -251,9 +253,16 @@ class DiskOperator(object):
continue
if device['MAJOR'] in constants.VALID_MAJOR_LIST:
device_node = device.device_node
if 'ID_PATH' in device:
device_path = "/dev/disk/by-path/" + device['ID_PATH']
LOG.debug("[DiskEnum] device_path: %s ", device_path)
elif (constants.DEVICE_NAME_MPATH in device.get("DM_NAME", "")
and 'DM_UUID' in device):
device_path = "/dev/disk/by-id/dm-uuid-" + device['DM_UUID']
LOG.debug("[DiskEnum] device_path: %s ", device_path)
device_node = utils.get_mpath_from_dm(device.device_node)
LOG.debug("[DiskEnum] device_node: %s ", device_node)
else:
# We should always have a udev supplied /dev/disk/by-path
# value as a matter of normal operation. We do not expect
@ -266,7 +275,7 @@ class DiskOperator(object):
# system.
device_path = None
LOG.error("Device %s does not have an ID_PATH value provided "
"by udev" % device.device_node)
"by udev" % device_node)
size_mib = 0
available_mib = 0
@ -276,14 +285,14 @@ class DiskOperator(object):
# Can merge all try/except in one block but this allows at
# least attributes with no exception to be filled
try:
size_mib = utils.get_disk_capacity_mib(device.device_node)
size_mib = utils.get_disk_capacity_mib(device_node)
except Exception as e:
self.handle_exception("Could not retrieve disk size - %s "
% e)
try:
available_mib = self.get_disk_available_mib(
device_node=device.device_node)
device_node=device_node)
except Exception as e:
self.handle_exception("Could not retrieve disk %s free space" % e)
@ -331,6 +340,8 @@ class DiskOperator(object):
try:
if 'ID_SCSI_SERIAL' in device:
serial_id = device['ID_SCSI_SERIAL']
elif constants.DEVICE_NAME_MPATH in device.get('DM_UUID', ''):
serial_id = device.get('DM_UUID').split('-')[1]
else:
serial_id = device['ID_SERIAL_SHORT']
except Exception as e:
@ -341,7 +352,7 @@ class DiskOperator(object):
if model_num:
capabilities.update({'model_num': model_num})
if self.get_rootfs_node() == device.device_node:
if self.get_rootfs_node() == device_node:
capabilities.update({'stor_function': 'rootfs'})
rotational = self.is_rotational(device)
@ -365,7 +376,7 @@ class DiskOperator(object):
device_id, device_wwn = self.get_device_id_wwn(device)
attr = {
'device_node': device.device_node,
'device_node': device_node,
'device_num': device.device_number,
'device_type': device_type,
'device_path': device_path,

View File

@ -93,8 +93,9 @@ class PartitionOperator(object):
partition_number)
else:
part_device_node = '{}{}'.format(device_node, partition_number)
part_device_path = '{}-part{}'.format(device_path,
partition_number)
part_device_path = utils.get_part_device_path(device_path,
partition_number)
start_mib = partition.get('start_mib')
end_mib = partition.get('end_mib')
@ -132,8 +133,13 @@ class PartitionOperator(object):
continue
if device['MAJOR'] in constants.VALID_MAJOR_LIST:
device_path = "/dev/disk/by-path/" + device['ID_PATH']
device_node = device.device_node
if 'ID_PATH' in device:
device_path = "/dev/disk/by-path/" + device['ID_PATH']
device_node = device.device_node
elif (constants.DEVICE_NAME_MPATH in device.get("DM_NAME", "")
and 'DM_UUID' in device):
device_path = "/dev/disk/by-id/dm-uuid-" + device['DM_UUID']
device_node = utils.get_mpath_from_dm(device.device_node)
try:
new_partitions = self.get_partition_info(device_path=device_path,

View File

@ -411,13 +411,14 @@ def _build_device_node_path(partition):
(idisk.device_node, len(partitions) + 1)
else:
device_node = "%s%s" % (idisk.device_node, len(partitions) + 1)
device_path = "%s-part%s" % (idisk.device_path, len(partitions) + 1)
device_path = cutils.get_part_device_path(idisk.device_path,
str(len(partitions) + 1))
else:
if constants.DEVICE_NAME_NVME in idisk.device_node:
device_node = idisk.device_node + "p1"
else:
device_node = idisk.device_node + '1'
device_path = idisk.device_path + '-part1'
device_path = cutils.get_part_device_path(idisk.device_path, "1")
return device_node, device_path

View File

@ -23,7 +23,6 @@ from eventlet.green import subprocess
import jsonpatch
import pecan
from pecan import rest
import re
import six
import wsme
from wsme import types as wtypes
@ -163,16 +162,16 @@ class Storage(base.APIBase):
for d in disks:
if (stor.journal_path is not None and
d.device_path is not None and
d.device_path in stor.journal_path):
partition_number = (re.match('.*?([0-9]+)$',
stor.journal_path).group(1))
cutils.is_part_of_disk(stor.journal_path,
d.device_path)):
part_number = cutils.get_part_number(stor.journal_path)
if (d.device_node is not None and
constants.DEVICE_NAME_NVME in d.device_node):
stor.journal_node = "{}p{}".format(d.device_node,
partition_number)
part_number)
else:
stor.journal_node = "{}{}".format(d.device_node,
partition_number)
part_number)
break
# never expose the ihost_id attribute, allow exposure for now
@ -1024,12 +1023,14 @@ def _create_journal(journal_location, journal_size_mib, stor):
# Determine if the journal partition is collocated or not.
if stor.uuid == journal_location:
# The collocated journal is always on /dev/sdX2.
journal_device_path = journal_onistor_idisk.device_path + "-part" + "2"
journal_device_path = cutils.get_part_device_path(
journal_onistor_idisk.device_path, "2")
else:
# Obtain the last partition index on which the journal will reside.
last_index = len(pecan.request.dbapi.journal_get_all(journal_location))
journal_device_path = (journal_onistor_idisk.device_path + "-part" +
str(last_index + 1))
journal_device_path = cutils.get_part_device_path(
journal_onistor_idisk.device_path,
str(last_index + 1))
journal_values = {'device_path': journal_device_path,
'size_mib': journal_size_mib,

View File

@ -808,6 +808,9 @@ DEVICE_TYPE_NVME = 'NVME'
DEVICE_TYPE_UNDETERMINED = 'Undetermined'
DEVICE_TYPE_NA = 'N/A'
DEVICE_NAME_NVME = 'nvme'
DEVICE_NAME_DM = 'dm-'
DEVICE_NAME_MPATH = 'mpath'
DEVICE_FS_TYPE_MPATH = 'mpath_member'
# Disk model types.
DEVICE_MODEL_UNKNOWN = 'Unknown'

View File

@ -48,6 +48,7 @@ import math
import os
import pathlib
import psutil
import pyudev
import pwd
import random
import re
@ -555,6 +556,7 @@ def is_system_usable_block_device(pydev_device):
Example devices:
o local block devices: local HDDs, SSDs, RAID arrays
o remote devices: iscsi mounted, LIO, EMC
o mpath partition and member devices
o non permanent devices: USB stick
:return bool: True if device can be used else False
"""
@ -564,6 +566,13 @@ def is_system_usable_block_device(pydev_device):
if pydev_device.get("DM_VG_NAME") or pydev_device.get("DM_LV_NAME"):
# Skip LVM devices
return False
if (constants.DEVICE_NAME_MPATH in pydev_device.get("DM_NAME", "")
and pydev_device.get("ID_PART_ENTRY_NAME")):
# Skip mpath partition devices
return False
if pydev_device.get("ID_FS_TYPE") == constants.DEVICE_FS_TYPE_MPATH:
# Skip mpath member devices
return False
id_path = pydev_device.get("ID_PATH", "")
if "iqn." in id_path or "eui." in id_path:
# Skip all iSCSI devices, they are links for volume storage.
@ -1633,7 +1642,7 @@ def is_partition_the_last(dbapi, partition):
"""
idisk_uuid = partition.get('idisk_uuid')
onidisk_parts = dbapi.partition_get_by_idisk(idisk_uuid)
part_number = re.match('.*?([0-9]+)$',
part_number = re.match('.*?-part([0-9]+)',
partition.get('device_path')).group(1)
if int(part_number) != len(onidisk_parts):
@ -3517,3 +3526,76 @@ def get_module_name_from_entry_point(entry_point):
raise exception.SysinvException(_(
"Module name for entry point {} "
"could not be determined.".format(entry_point)))
def get_mpath_from_dm(dm_device):
"""Get mpath node from /dev/dm-N"""
mpath_device = None
context = pyudev.Context()
pydev_device = pyudev.Device.from_device_file(context, dm_device)
if constants.DEVICE_NAME_MPATH in pydev_device.get("DM_NAME", ""):
re_line = re.compile(r'^(\D*)')
match = re_line.search(pydev_device.get("DM_NAME"))
if match:
mpath_device = os.path.join("/dev/mapper", match.group(1))
return mpath_device
def get_part_device_path(disk_device_path, part_number):
"""Get the partition device path.
:param disk_device_path: the device path of the disk on which the
partition resides
:param part_number: the partition number
:returns the partition device path
"""
if constants.DEVICE_NAME_MPATH in disk_device_path:
path_split = disk_device_path.split(constants.DEVICE_NAME_MPATH)
part_device_path = '{}part{}-{}{}'.format(path_split[0],
part_number,
constants.DEVICE_NAME_MPATH,
path_split[1])
else:
part_device_path = '{}-part{}'.format(disk_device_path, part_number)
return part_device_path
def get_part_number(part_device_path):
"""Obtain the number of a partition.
:param part_device_path: the partition's device path
:returns the partition's number
"""
part_num = ""
if 'by-path' in part_device_path:
part_num = re.match('.*?([0-9]+)$', part_device_path).group(1)
if constants.DEVICE_NAME_MPATH in part_device_path:
match_path = re.match('(/dev/disk/by-id/.+)-part([0-9]+)(-mpath.*)',
part_device_path)
if match_path:
part_num = match_path.group(2)
return part_num
def is_part_of_disk(part_device_path, disk_device_path):
"""Check if a partition is part of a disk
:param part_device_path: the partition's device path
:param disk_device_path: the disk's device path
:returns the partition's number
"""
is_part_of_disk = False
if disk_device_path in part_device_path:
is_part_of_disk = True
elif constants.DEVICE_NAME_MPATH in disk_device_path:
path_split = disk_device_path.split(constants.DEVICE_NAME_MPATH)
if (path_split[0] in part_device_path and
path_split[1] in part_device_path):
is_part_of_disk = True
return is_part_of_disk

View File

@ -4228,10 +4228,11 @@ class ConductorManager(service.PeriodicService):
idisk_uuid, sort_key='device_path')
if partitions:
device_node = "%s%s" % (idisk.device_node, len(partitions) + 1)
device_path = "%s-part%s" % (idisk.device_path, len(partitions) + 1)
device_path = cutils.get_part_device_path(idisk.device_path,
str(len(partitions) + 1))
else:
device_node = idisk.device_node + '1'
device_path = idisk.device_path + '-part1'
device_path = cutils.get_part_device_path(idisk.device_path, "1")
return device_node, device_path
@ -4507,8 +4508,18 @@ class ConductorManager(service.PeriodicService):
continue
# Obtain the disk the partition is on.
part_disk = next((d for d in db_disks
if d.device_path in db_part.device_path), None)
part_disk = None
for d in db_disks:
if d.device_path in db_part.device_path:
part_disk = d
break
elif constants.DEVICE_NAME_MPATH in d.device_node:
path_split = d.device_path.split(constants.DEVICE_NAME_MPATH)
if (path_split[0] in db_part.device_path and
path_split[1] in db_part.device_path):
is_part_of_disk = True
part_disk = d
break
if not part_disk:
# Should not happen as we only store partitions associated
@ -4581,7 +4592,14 @@ class ConductorManager(service.PeriodicService):
LOG.debug("PART conductor - partition not found, adding...")
# Complete disk info.
for db_disk in db_disks:
is_part_of_disk = False
if db_disk.device_path in ipart['device_path']:
is_part_of_disk = True
elif constants.DEVICE_NAME_MPATH in db_disk.device_node:
path_split = db_disk.device_path.split(constants.DEVICE_NAME_MPATH)
if path_split[0] in ipart['device_path'] and path_split[1] in ipart['device_path']:
is_part_of_disk = True
if is_part_of_disk:
part_dict.update({'idisk_id': db_disk.id,
'idisk_uuid': db_disk.uuid})
LOG.debug("PART conductor - disk - part_dict: %s " %
@ -12338,7 +12356,7 @@ class ConductorManager(service.PeriodicService):
"host %s " % active_controller.hostname))
# The partition for cinder volumes is always the first.
cinder_device_partition = '{}{}'.format(cinder_device, '-part1')
cinder_device_partition = cutils.get_part_device_path(cinder_device, '1')
cinder_size = self.get_partition_size(context, cinder_device_partition)
return cinder_size

View File

@ -21,7 +21,6 @@
import eventlet
import re
from oslo_config import cfg
from oslo_db import exception as db_exc
@ -2987,11 +2986,9 @@ class Connection(api.Connection):
# Update the journal device path.
journals = self.journal_get_all(stor.uuid)
for journal in journals:
partition_number = re.match('.*?([0-9]+)$',
journal.device_path).group(1)
device_path = "{}{}{}".format(disk['device_path'],
"-part",
partition_number)
partition_number = utils.get_part_number(journal.device_path)
device_path = utils.get_part_device_path(disk['device_path'],
partition_number)
updates = {'device_path': device_path}
self.journal_update(journal['uuid'], updates)
@ -3015,8 +3012,9 @@ class Connection(api.Connection):
for journal in journals:
# Update DB
journal_path = journal_disk.device_path
updates = {'device_path': journal_path + "-part" +
str(partition_index)}
updates = {'device_path':
utils.get_part_device_path(journal_path,
str(partition_index))}
self.journal_update(journal.id, updates)
partition_index += 1
# Update output
@ -3062,8 +3060,9 @@ class Connection(api.Connection):
for journal in journals:
stor = self.istor_get(journal.foristorid)
disk = self.idisk_get_by_istor(stor.uuid)[0]
device_path = utils.get_part_device_path(disk.device_path, "2")
journal_vals = {'onistor_uuid': stor.uuid,
'device_path': disk.device_path + "-part" + "2",
'device_path': device_path,
'size_mib': CONF.journal.journal_default_size}
self.journal_update(journal.id, journal_vals)
@ -3153,8 +3152,8 @@ class Connection(api.Connection):
if value == istor_obj['uuid']:
# If the journal becomes collocated, assign second
# partition.
journal_vals['device_path'] = new_onidisk.device_path + \
"-part" + "2"
journal_vals['device_path'] = utils.get_part_device_path(
new_onidisk.device_path, "2")
del values[key]

View File

@ -338,11 +338,12 @@ class CephPuppet(openstack.OpenstackBasePuppet):
if stor.function == constants.STOR_FUNCTION_OSD:
# platform_ceph_osd puppet resource parameters
data_path = utils.get_part_device_path(disk.device_path, '1')
osd = {
'osd_id': stor.osdid,
'osd_uuid': stor.uuid,
'disk_path': disk.device_path,
'data_path': disk.device_path + '-part1',
'data_path': data_path,
'journal_path': stor.journal_path,
'tier_name': stor.tier_name,
}

View File

@ -8,6 +8,7 @@ import json
import re
from sysinv.common import constants
from sysinv.common import utils
from sysinv.puppet import base
@ -299,7 +300,7 @@ class StoragePuppet(base.BasePuppet):
# add the disk partition to the disk path
partition_number = re.match('.*?([0-9]+)$',
pv.lvm_pv_name).group(1)
pv_path += "-part%s" % partition_number
pv_path = utils.get_part_device_path(pv_path, partition_number)
if (pv.pv_state == constants.PV_ADD):
adding_pvs.append(pv_path)

View File

@ -50,7 +50,7 @@ class TestPartition(base.FunctionalTest):
API_HEADERS = {'User-Agent': 'sysinv-test'}
disk_device_path = '/dev/disk/by-path/pci-0000:00:0d.0-ata-1.0'
partition_device_path = '/dev/disk/by-path/pci-0000:00:0d.0-ata-1.1'
partition_device_path = '/dev/disk/by-path/pci-0000:00:0d.0-ata-1.0-part1'
def setUp(self):
super(TestPartition, self).setUp()