2
0
mirror of https://github.com/xcat2/confluent.git synced 2026-08-03 07:57:02 +00:00

Implement deployment.storage

This is an attribute for a node to indicate preferences for storage.

For now, 'm2' policy will hit m.2 and mirroring kits.
This commit is contained in:
Jarrod Johnson
2026-07-28 15:02:27 -04:00
parent 3501f70c37
commit d7dcb07a3f
3 changed files with 229 additions and 18 deletions
@@ -1,6 +1,9 @@
#!/usr/bin/python3
import json
import subprocess
import os
import shutil
class SilentException(Exception):
pass
@@ -71,6 +74,10 @@ class DiskInfo(object):
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('"', '')
if not self.driver and 'imsm' not in self.mdcontainer and self.subsystype != 'nvm':
raise Exception("No driver detected")
if self.driver == 'sr':
@@ -106,6 +113,114 @@ class DiskInfo(object):
})
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():
if 'm.2 socket' not in slot.get('type', '').lower():
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().lower()
for disk in list(disks):
if spec == 'm2':
if disk.model.lower() in PRIORITY_MODELS:
continue
if not getattr(disk, 'busaddr', None):
disks.remove(disk)
continue
if slotinfo is None:
slotinfo = get_m2_slot_info()
if disk.busaddr not in slotinfo:
disks.remove(disk)
elif spec.startswith('/dev/'):
bspec = os.path.basename(spec)
if disk.name.lower() == bspec:
return [disk]
else:
raise Exception("Unknown deployment.storage specification: {0}".format(storagespec))
if not disks:
raise Exception("No disks match deployment.storage specification: {0}".format(storagespec))
return disks
def main():
disks = []
for disk in sorted(os.listdir('/sys/class/block')):
@@ -116,9 +231,13 @@ def main():
pass
except Exception as e:
print("Skipping {0}: {1}".format(disk, str(e)))
slotinfo = get_m2_slot_info()
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()
@@ -1,12 +1,88 @@
#!/usr/bin/python3
import subprocess
import json
import os
class SilentException(Exception):
pass
def list_slots():
slots = {}
current = None
dump = subprocess.check_output(['smbiosDump'])
if not isinstance(dump, str):
dump = dump.decode('utf8')
for line in dump.split('\n'):
if 'System Slot (Type 9):' in line:
current = {}
elif current is not None:
stripped = line.strip()
if stripped.startswith('Designation:'):
current['designation'] = stripped.split(':', 1)[1].strip().strip('"')
if stripped.startswith('Type:'):
current['type_label'] = stripped.split(':', 1)[1].strip()
current['type_number'] = int(stripped.split(':', 1)[1].strip().split(' ')[0], 16)
# 0x14 through 0x17 are M.2 slots, inclusively
if current['type_number'] in (0x14, 0x15, 0x16, 0x17):
current['is_m2'] = True
else:
current['is_m2'] = False
elif stripped.startswith('PCI Address:'):
addr = stripped.split(':', 1)[1].strip()
current['pci_address'] = addr
slots[addr] = current
current = None
return slots
def list_m2_slots():
slots = list_slots()
m2slots = {}
for addr in slots:
slot = slots[addr]
if slot.get('is_m2', False):
m2slots[addr] = 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
# 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',
'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')
class DiskInfo(object):
def __init__(self, devname, devinfo):
def __init__(self, devname, devinfo, storagespec=None):
self.name = devname
self.path = '/dev/' + devname
self.wwn = None
@@ -23,13 +99,16 @@ class DiskInfo(object):
raise SilentException("CD-ROM device")
if self.size < 2048:
raise SilentException("Too small")
if storagespec and storagespec == 'm2':
if self.model.lower() not in PRIORITY_MODELS and not devinfo.get('m2_slot', None):
raise SilentException("Not an M.2 disk")
@property
def priority(self):
if self.model.lower() in ('m.2 nvme 2-bay raid kit', 'thinksystem_m.2_vd', 'thinksystem m.2', 'thinksystem_m.2'):
if self.model.lower() in PRIORITY_MODELS:
return 0
if self.driver == 'vmw_ahci':
return 2
@@ -49,6 +128,7 @@ class DiskInfo(object):
def list_disks():
current_dev = None
m2slots = list_m2_slots()
disks = {}
devlist = subprocess.check_output(['localcli', 'storage', 'core', 'device', 'list'])
if not isinstance(devlist, str):
@@ -96,31 +176,40 @@ def list_disks():
adp = ' '.join(line.split()[1:])
disks[dev]['adapter'] = adp
devbyadp.setdefault(adp, []).append(dev)
adapterlist = subprocess.check_output(['localcli', 'storage', 'core', 'adapter', 'list'])
adapterlist = subprocess.check_output(['localcli', 'storage', 'core', 'adapter', 'listdetailed'])
if not isinstance(adapterlist, str):
adapterlist = adapterlist.decode('utf8')
driverbyadp = {}
linenum = 0
adapters = {}
curradpinfo = {}
for line in adapterlist.split('\n'):
linenum += 1
line = line.rstrip()
if not line.strip():
curradpinfo = {}
continue
if linenum < 3:
continue
parts = line.split()
if len(parts) < 2:
continue
adp = parts[0]
driver = parts[1]
driverbyadp[adp] = driver
if not line.startswith(' '):
curradpname = line.split(':', 1)[0].strip()
curradpinfo['name'] = curradpname
adapters[curradpname] = curradpinfo
else:
if ' Driver:' in line:
curradpinfo['driver'] = line.split(maxsplit=1)[1]
elif ' Description:' in line:
curradpinfo['description'] = line.split(maxsplit=1)[1]
if curradpinfo['description'].startswith('('):
curradpinfo['pcieaddress'] = curradpinfo['description'].split('(')[1].split(')')[0]
for adp in devbyadp:
driver = driverbyadp.get(adp, 'Unknown')
driver = adapters.get(adp, {}).get('driver', 'Unknown')
pcieaddr = adapters.get(adp, {}).get('pcieaddress', None)
for dev in devbyadp[adp]:
disks[dev]['adapter_driver'] = driver
disks[dev]['adapter_pcieaddress'] = pcieaddr
if pcieaddr and pcieaddr in m2slots:
disks[dev]['m2_slot'] = m2slots[pcieaddr]
return disks
def main():
disks = []
storagespec = get_deployment_storage()
try:
alldisks = list_disks()
except Exception as e:
@@ -128,7 +217,7 @@ def main():
alldisks = {}
for disk in alldisks:
try:
disks.append(DiskInfo(disk, alldisks[disk]))
disks.append(DiskInfo(disk, alldisks[disk], storagespec=storagespec))
except SilentException:
pass
except Exception as e:
@@ -255,6 +255,9 @@ node = {
'deployment.state_last_updated': {
'description': ('Timestamp of last state change, as reported by an OS profile, when available'),
},
'deployment.storage': {
'description': ('Indicates the storage to use for OS deployment. This may be a specific device name, or a more generic description such as "m2" to indicate that the OS should be deployed to an M.2 device. If not specified, the default behavior is to prioritize according to various criteria and use the first disk based on that evaluation.'),
},
'deployment.useinsecureprotocols': {
'description': ('What phase(s) of boot are permitted to use insecure protocols '
'(TFTP and HTTP without TLS. By default, only HTTPS is used. However '