2
0
mirror of https://github.com/xcat2/confluent.git synced 2026-11-12 11:06:17 +00:00
Files
confluent/confluent_osdeploy/common/profile/scripts/getinstalldisk
T
Markus Hilger 4aa6bf8af5 Accept plain device names in deployment.storage and fail on no match
The attribute help promises "a specific device name", but getinstalldisk
only understood "m2" and "/dev/<name>". A plain "nvme0n1" aborted the
install with "Unknown deployment.storage specification", and a
"/dev/<name>" that matched no disk was silently ignored, so the OS went
to whatever disk the default ordering picked. For an attribute meant to
pin the OS disk that is the dangerous failure mode.

Accept a kernel name with or without /dev/ and resolve /dev/disk/by-*
links to their kernel name, so a disk can be pinned by a stable path.
When nothing matches, fail with "No disks match" like the m2 filter
already does. Document the accepted forms in the attribute help.
2026-10-30 09:43:54 +01:00

313 lines
11 KiB
Python

#!/usr/bin/python3
import json
import subprocess
import os
import shutil
class SilentException(Exception):
pass
# List of models that are generally used for OS install disk (generally 2-disk mirroring)
PRIORITY_MODELS = ('m.2 nvme 2-bay raid kit',
'thinksystem_m.2_vd',
'thinksystem m.2',
'thinksystem_m.2',
'thinksystem 7mm',
'thinksystem_7mm',
'thinksystem_7mm_vd',
'b540p-2hs', # 7mm or M.2 in rear
'b540d-2hs', # M.2 in E3.S or 2.5inch
'b550d-2hs', # Front M.2
'b550p-2hs', # Rear M.2
'raid b540p-2hs',
'raid b540d-2hs',
'raid b550d-2hs',
'raid b550p-2hs',
'raid b540i-2i',
'raid_b540i-2i',
'raid b545i-2i',
'raid b545i-2i',
'raid_b545-2i')
BOOT_SLOT_KEYWORDS = ('m.2 socket', '7mm')
class DiskInfo(object):
def __init__(self, devname, slotinfo=None):
if devname.startswith('nvme') and 'c' in devname:
raise Exception("Skipping multipath devname")
self.name = devname
self.wwn = None
self.path = None
self.model = ''
self.size = 0
self.driver = ''
self.mdcontainer = ''
self.subsystype = ''
self.kernnames = []
self.vrocmembers = []
self.is_m2 = False
self.in_vroc = False
devnode = '/dev/{0}'.format(devname)
qprop = subprocess.check_output(
['udevadm', 'info', '--query=property', devnode])
if not isinstance(qprop, str):
qprop = qprop.decode('utf8')
for prop in qprop.split('\n'):
if '=' not in prop:
continue
k, v = prop.split('=', 1)
if k == 'DEVTYPE' and v != 'disk':
if v == 'partition':
raise SilentException('Partition')
raise Exception('Not a disk')
elif k == 'DM_NAME':
raise SilentException('Device Mapper')
elif k == 'ID_MODEL':
self.model = v
elif k == 'DEVPATH':
self.path = v
elif k == 'ID_WWN':
self.wwn = v
elif k == 'MD_CONTAINER':
self.mdcontainer = v
attrs = subprocess.check_output(['udevadm', 'info', '-a', devnode])
if not isinstance(attrs, str):
attrs = attrs.decode('utf8')
for attr in attrs.split('\n'):
if '==' not in attr:
continue
k, v = attr.split('==', 1)
k = k.strip()
if k == 'ATTRS{size}':
self.size = v.replace('"', '')
elif (k == 'DRIVERS' and not self.driver
and v not in ('"sd"', '""')):
self.driver = v.replace('"', '')
elif k == 'ATTRS{subsystype}':
self.subsystype = v.replace('"', '')
elif k == 'ATTR{ro}' and v == '"1"':
raise Exception("Device is read-only")
elif k == 'ATTRS{removable}' and v == '"1"':
raise Exception("Device is removable")
elif k == 'ATTRS{address}':
self.busaddr = v.replace('"', '')
elif k == 'KERNELS':
self.kernnames.append(v.replace('"', ''))
if not getattr(self, 'busaddr', None):
for kernname in getattr(self, 'kernnames', []):
if kernname in slotinfo:
self.is_m2 = True
break
if getattr(self, 'busaddr', None) and self.busaddr in slotinfo:
self.is_m2 = True
if not self.driver and 'imsm' not in self.mdcontainer and self.subsystype != 'nvm':
raise Exception("No driver detected")
if 'imsm' in self.mdcontainer:
try:
mdinfo = subprocess.check_output(
['mdadm', '--detail', '-Y', devnode],
stderr=subprocess.DEVNULL)
if not isinstance(mdinfo, str):
mdinfo = mdinfo.decode('utf8', errors='ignore')
for line in mdinfo.splitlines():
key, separator, value = line.partition('=')
if separator and key.endswith('_DEV'):
member = value.strip()
member = member.replace('/dev/', '')
if member and member not in self.vrocmembers:
self.vrocmembers.append(member)
except Exception:
pass
if self.driver == 'sr':
raise Exception('cd/dvd')
if os.path.exists('/sys/block/{0}/size'.format(self.name)):
with open('/sys/block/{0}/size'.format(self.name), 'r') as sizesrc:
self.size = int(sizesrc.read()) * 512
if int(self.size) < 2147483648:
raise Exception("Device too small for install ({}MiB)".format(int(self.size)/1024/1024))
@property
def priority(self):
if self.in_vroc:
return 99 # prefer the assembled array over the member disks
if self.is_m2:
return 0
if self.model.lower() in PRIORITY_MODELS:
return 0
if 'imsm' in self.mdcontainer:
return 1
if self.driver == 'ahci':
return 2
if self.driver.startswith('megaraid'):
return 3
if self.driver.startswith('mpt'):
return 4
return 99
def __repr__(self):
return repr({
'name': self.name,
'path': self.path,
'wwn': self.wwn,
'driver': self.driver,
'size': self.size,
'model': self.model,
})
def get_dmi_type9_info():
dmidecode = shutil.which('dmidecode')
if not dmidecode:
return {}
try:
dmio = subprocess.check_output(
[dmidecode, '--type', '9'], stderr=subprocess.DEVNULL)
except Exception:
return {}
if not isinstance(dmio, str):
dmio = dmio.decode('utf8', errors='ignore')
slots = {}
currentslot = None
for line in dmio.split('\n'):
stripped = line.strip()
if not stripped:
if currentslot:
busaddr = currentslot.get('bus_address', None)
if busaddr:
slots[busaddr] = currentslot
currentslot = None
continue
if stripped.startswith('Handle ') and ', DMI type 9,' in stripped:
if currentslot:
busaddr = currentslot.get('bus_address', None)
if busaddr:
slots[busaddr] = currentslot
currentslot = {'handle': stripped.split(',', 1)[0].split(' ', 1)[1]}
continue
if currentslot is None:
continue
if ':' not in stripped:
continue
key, val = stripped.split(':', 1)
key = key.strip().lower().replace(' ', '_')
currentslot[key] = val.strip()
if currentslot:
busaddr = currentslot.get('bus_address', None)
if busaddr:
slots[busaddr] = currentslot
return slots
def get_m2_slot_info():
slots = get_dmi_type9_info()
m2slots = {}
for slot in slots.values():
label = '{0} {1}'.format(
slot.get('type', ''), slot.get('designation', '')).lower()
if not any(x in label for x in BOOT_SLOT_KEYWORDS):
continue
busaddr = slot.get('bus_address', None)
if not busaddr:
continue
m2slots[busaddr] = slot
return m2slots
def get_deployment_storage():
apiclient = None
for clientpath in ('/opt/confluent/bin/apiclient', '/etc/confluent/apiclient'):
if os.path.exists(clientpath):
apiclient = clientpath
break
if not apiclient:
return None
for attriburl in ('/confluent-api/self/myattrib', '/confluent-api/self/myattribs'):
try:
attribs = subprocess.check_output(
['python3', apiclient, attriburl, '-j'], stderr=subprocess.DEVNULL)
if not isinstance(attribs, str):
attribs = attribs.decode('utf8')
attribs = json.loads(attribs)
if not isinstance(attribs, dict):
continue
storage = attribs.get('deployment.storage', None)
if isinstance(storage, str) and storage.strip():
return storage.strip()
except Exception:
continue
return None
def filter_deployment_storage(disks, storagespec, slotinfo=None):
if not storagespec:
return disks
spec = storagespec.strip()
if spec.lower() != 'm2':
# A device name with or without /dev/, or a persistent link such as
# /dev/disk/by-path/..., resolved to its kernel name
if spec.startswith('/dev/'):
spec = os.path.realpath(spec)
devname = os.path.basename(spec).lower()
for disk in disks:
if disk.name.lower() == devname:
return [disk]
raise Exception("No disks match deployment.storage specification: {0}".format(storagespec))
for disk in list(disks):
if disk.is_m2:
continue
if disk.model.lower() in PRIORITY_MODELS:
continue
if slotinfo is None:
slotinfo = get_m2_slot_info()
if not getattr(disk, 'busaddr', None):
for kernname in getattr(disk, 'kernnames', []):
if kernname in slotinfo:
break
else:
disks.remove(disk)
continue
if disk.busaddr not in slotinfo:
disks.remove(disk)
if not disks:
raise Exception("No disks match deployment.storage specification: {0}".format(storagespec))
return disks
def attach_member_disks(disks):
for disk in disks:
num_vroc_members = len(getattr(disk, 'vrocmembers', []))
for vrocmember in getattr(disk, 'vrocmembers', []):
for memberdisk in disks:
if memberdisk.name == vrocmember:
memberdisk.in_vroc = True
if memberdisk.is_m2:
disk.is_m2 = True
else:
if num_vroc_members == 2 and 'nvme' in vrocmember:
# This is not assured, but is likely
# Unfortunately, some VMD configurations blow past any way to detect form factor
# as DMI table may not include
disk.is_m2 = True
def main():
disks = []
slotinfo = get_m2_slot_info()
for disk in sorted(os.listdir('/sys/class/block')):
try:
disk = DiskInfo(disk, slotinfo=slotinfo)
disks.append(disk)
except SilentException:
pass
except Exception as e:
print("Skipping {0}: {1}".format(disk, str(e)))
attach_member_disks(disks)
disks = filter_deployment_storage(disks, get_deployment_storage(), slotinfo=slotinfo)
nd = [x.name for x in sorted(disks, key=lambda x: [x.priority, x.size])]
if nd:
open('/tmp/installdisk', 'w').write(nd[0])
else:
raise Exception("No suitable install disk found")
if __name__ == '__main__':
main()