diff --git a/confluent_osdeploy/common/profile/scripts/getinstalldisk b/confluent_osdeploy/common/profile/scripts/getinstalldisk index cb883c9d..7ab75499 100644 --- a/confluent_osdeploy/common/profile/scripts/getinstalldisk +++ b/confluent_osdeploy/common/profile/scripts/getinstalldisk @@ -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() diff --git a/confluent_osdeploy/esxi7/profiles/hypervisor/scripts/getinstalldisk b/confluent_osdeploy/esxi7/profiles/hypervisor/scripts/getinstalldisk index 3c780361..419f5224 100644 --- a/confluent_osdeploy/esxi7/profiles/hypervisor/scripts/getinstalldisk +++ b/confluent_osdeploy/esxi7/profiles/hypervisor/scripts/getinstalldisk @@ -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: diff --git a/confluent_server/confluent/config/attributes.py b/confluent_server/confluent/config/attributes.py index 75999910..72ceaced 100644 --- a/confluent_server/confluent/config/attributes.py +++ b/confluent_server/confluent/config/attributes.py @@ -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 '