From 44405baaf39af36d1b695d1f8be6836319257816 Mon Sep 17 00:00:00 2001 From: weragrzeda Date: Wed, 16 Aug 2023 14:30:05 +0200 Subject: [PATCH 001/146] nodeconfig ntp servers for smms --- confluent_client/bin/nodeconfig | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/confluent_client/bin/nodeconfig b/confluent_client/bin/nodeconfig index 06d512c7..0ba8d351 100755 --- a/confluent_client/bin/nodeconfig +++ b/confluent_client/bin/nodeconfig @@ -1,4 +1,4 @@ -#!/usr/bin/python2 +#!/usr/libexec/platform-python # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2017 Lenovo @@ -98,8 +98,17 @@ cfgpaths = { 'static_v6_gateway'), 'bmc.hostname': ( 'configuration/management_controller/hostname', 'hostname'), + 'bmc.ntp': ( + 'configuration/management_controller/ntp/enabled', 'state'), + 'bmc.ntpServer1': ( + 'configuration/management_controller/ntp/servers/1', 'server'), + 'bmc.ntpServer2': ( + 'configuration/management_controller/ntp/servers/2', 'server'), + 'bmc.ntpServer3': ( + 'configuration/management_controller/ntp/servers/3', 'server') } + autodeps = { 'bmc.ipv4_address': (('bmc.ipv4_method', 'static'),) } @@ -113,6 +122,7 @@ client.check_globbing(noderange) setmode = None assignment = {} queryparms = {} + printsys = [] printbmc = [] printextbmc = [] @@ -131,7 +141,6 @@ if len(args) == 1 or options.exclude: queryparms[path] = {} queryparms[path][attrib] = candidate - def _assign_value(): if key not in cfgpaths: setsys[key] = value @@ -233,6 +242,7 @@ else: parse_config_line(args[1:]) session = client.Command() rcode = 0 + if options.restoredefault: session.stop_if_noderange_over(noderange, options.maxnodes) if options.restoredefault.lower() in ( @@ -295,8 +305,10 @@ else: for path in queryparms: if options.comparedefault: continue - rcode |= client.print_attrib_path(path, session, list(queryparms[path]), - NullOpt(), queryparms[path]) + + rcode |= client.print_attrib_path(path, session, list(queryparms[path]),NullOpt(), queryparms[path]) + + if printsys == 'all' or printextbmc or printbmc or printallbmc: if printbmc or not printextbmc: rcode |= client.print_attrib_path( From 6d87d11f5e6decd854995d4b5bd7188c049c418a Mon Sep 17 00:00:00 2001 From: weragrzeda Date: Wed, 23 Aug 2023 14:28:33 +0200 Subject: [PATCH 002/146] my changes to Eaton PDU sensors --- .../plugins/hardwaremanagement/eatonpdu.py | 49 ++++++++++++++----- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/confluent_server/confluent/plugins/hardwaremanagement/eatonpdu.py b/confluent_server/confluent/plugins/hardwaremanagement/eatonpdu.py index 16be5b38..da60b182 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/eatonpdu.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/eatonpdu.py @@ -158,7 +158,7 @@ def get_sensor_data(element, node, configmanager): sn = _sensors_by_node.get(node, None) for outlet in sn[0]: for sensename in sn[0][outlet]: - myname = 'Outlet {0} {1}'.format(outlet, sensename) + myname = '{0} {1}'.format(outlet, sensename) measurement = sn[0][outlet][sensename] if name == 'all' or simplify_name(myname) == name: readings.append({ @@ -259,6 +259,11 @@ class PDUClient(object): url = '/config/gateway?page={}&sessionId={}&_dc={}'.format(suburl, self.sessid, int(time.time())) return wc.grab_response(url) + def do_request1(self, suburl): + wc = self.wc + url = '/config/gateway?page={}&sessionId={}'.format(suburl, self.sessid) + return wc.grab_response(url) + def logout(self): self.do_request('cgi_logout') @@ -274,29 +279,51 @@ class PDUClient(object): return def get_sensor_data(self): - rsp = self.do_request('cgi_pdu_outlets') + rsp = self.do_request1('cgi_overview') + data = sanitize_json(rsp[0]) data = json.loads(data) + data1 = data['data'][4][0][8] data = data['data'][0] sdata = {} for outdata in data: + outsense = {} - outletname = outdata[0][0] - outsense['Energy'] = { - 'value': float(outdata[11] / 1000), - 'units': 'kwh', - 'type': 'Energy' - } + outletname = outdata[3] outsense['Power'] = { - 'value': float(outdata[4]), - 'units': 'w', + 'value': outdata[5], + 'units': 'kW', 'type': 'Power', } sdata[outletname] = outsense + for outdata in data1: + + outsense = {} + outletname = outdata[0] + if type(outdata[1]) == str : + splitter = outdata[1].split(" ") + + if len(splitter) == 1: + splitter.append('w') + outsense['Power'] = { + 'value': splitter[0], + 'units': splitter[1], + 'type': 'Power', + } + elif type(outdata[1]) == list: + if type(outdata[1][1]) == float: + outletname=outletname.strip('
Since') + + outsense['Energy'] = { + 'value': outdata[1][0] / 1000, + 'units': 'kWh', + 'type': 'Energy', + } + sdata[outletname] = outsense return sdata def set_outlet(self, outlet, state): - rsp = self.do_request('cgi_pdu_outlets') + rsp = self.do_request('cgi_overview') data = sanitize_json(rsp[0]) data = json.loads(data) data = data['data'][0] From f633c93d0f6dc45af805c3830e02c596cefe74a7 Mon Sep 17 00:00:00 2001 From: weragrzeda Date: Fri, 25 Aug 2023 08:27:27 +0200 Subject: [PATCH 003/146] Geit PDU nodeinvntory Please enter the commit message for your changes. Lines starting with '' will be ignored, and an empty message aborts the commit. On branch master Your branch is up to date with 'origin/master'. Changes to be committed: modified: geist.py --- .../plugins/hardwaremanagement/geist.py | 264 +++++++++++------- 1 file changed, 166 insertions(+), 98 deletions(-) diff --git a/confluent_server/confluent/plugins/hardwaremanagement/geist.py b/confluent_server/confluent/plugins/hardwaremanagement/geist.py index 3af6fa49..64bf04f3 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/geist.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/geist.py @@ -20,23 +20,28 @@ import eventlet.green.time as time import eventlet import eventlet.greenpool as greenpool + + def simplify_name(name): - return name.lower().replace(' ', '_').replace('/', '-').replace( - '_-_', '-') + return name.lower().replace(" ", "_").replace("/", "-").replace("_-_", "-") + pdupool = greenpool.GreenPool(128) + def data_by_type(indata): databytype = {} for keyname in indata: obj = indata[keyname] - objtype = obj.get('type', None) + objtype = obj.get("type", None) if not objtype: continue if objtype in databytype: - raise Exception("Multiple instances of type {} not yet supported".format(objtype)) + raise Exception( + "Multiple instances of type {} not yet supported".format(objtype) + ) databytype[objtype] = obj - obj['keyname'] = keyname + obj["keyname"] = keyname return databytype @@ -58,173 +63,227 @@ class GeistClient(object): def wc(self): if self._wc: return self._wc - targcfg = self.configmanager.get_node_attributes(self.node, - ['hardwaremanagement.manager'], - decrypt=True) + targcfg = self.configmanager.get_node_attributes( + self.node, ["hardwaremanagement.manager"], decrypt=True + ) targcfg = targcfg.get(self.node, {}) - target = targcfg.get( - 'hardwaremanagement.manager', {}).get('value', None) + target = targcfg.get("hardwaremanagement.manager", {}).get("value", None) if not target: target = self.node - target = target.split('/', 1)[0] + target = target.split("/", 1)[0] cv = util.TLSCertVerifier( - self.configmanager, self.node, - 'pubkeys.tls_hardwaremanager').verify_cert + self.configmanager, self.node, "pubkeys.tls_hardwaremanager" + ).verify_cert self._wc = wc.SecureHTTPConnection(target, port=443, verifycallback=cv) return self._wc def login(self, configmanager): - credcfg = configmanager.get_node_attributes(self.node, - ['secret.hardwaremanagementuser', - 'secret.hardwaremanagementpassword'], - decrypt=True) + credcfg = configmanager.get_node_attributes( + self.node, + ["secret.hardwaremanagementuser", "secret.hardwaremanagementpassword"], + decrypt=True, + ) credcfg = credcfg.get(self.node, {}) - username = credcfg.get( - 'secret.hardwaremanagementuser', {}).get('value', None) - passwd = credcfg.get( - 'secret.hardwaremanagementpassword', {}).get('value', None) + username = credcfg.get("secret.hardwaremanagementuser", {}).get("value", None) + passwd = credcfg.get("secret.hardwaremanagementpassword", {}).get("value", None) if not isinstance(username, str): - username = username.decode('utf8') + username = username.decode("utf8") if not isinstance(passwd, str): - passwd = passwd.decode('utf8') + passwd = passwd.decode("utf8") if not username or not passwd: - raise Exception('Missing username or password') + raise Exception("Missing username or password") self.username = username rsp = self.wc.grab_json_response( - '/api/auth/{0}'.format(username), - {'cmd': 'login', 'data': {'password': passwd}}) - token = rsp['data']['token'] + "/api/auth/{0}".format(username), + {"cmd": "login", "data": {"password": passwd}}, + ) + token = rsp["data"]["token"] return token def logout(self): if self._token: - self.wc.grab_json_response('/api/auth/{0}'.format(self.username), - {'cmd': 'logout', 'token': self.token}) + self.wc.grab_json_response( + "/api/auth/{0}".format(self.username), + {"cmd": "logout", "token": self.token}, + ) self._token = None def get_outlet(self, outlet): - rsp = self.wc.grab_json_response('/api/dev') - rsp = rsp['data'] + rsp = self.wc.grab_json_response("/api/dev") + rsp = rsp["data"] dbt = data_by_type(rsp) - if 't3hd' in dbt: - del dbt['t3hd'] + if "t3hd" in dbt: + del dbt["t3hd"] if len(dbt) != 1: - raise Exception('Multiple PDUs not supported per pdu') + raise Exception("Multiple PDUs not supported per pdu") pdutype = list(dbt)[0] - outlet = dbt[pdutype]['outlet'][str(int(outlet) - 1)] - state = outlet['state'].split('2')[-1] + outlet = dbt[pdutype]["outlet"][str(int(outlet) - 1)] + state = outlet["state"].split("2")[-1] return state def set_outlet(self, outlet, state): - rsp = self.wc.grab_json_response('/api/dev') - dbt = data_by_type(rsp['data']) - if 't3hd' in dbt: - del dbt['t3hd'] + rsp = self.wc.grab_json_response("/api/dev") + dbt = data_by_type(rsp["data"]) + if "t3hd" in dbt: + del dbt["t3hd"] if len(dbt) != 1: self.logout() - raise Exception('Multiple PDUs per endpoint not supported') - pdu = dbt[list(dbt)[0]]['keyname'] + raise Exception("Multiple PDUs per endpoint not supported") + pdu = dbt[list(dbt)[0]]["keyname"] outlet = int(outlet) - 1 rsp = self.wc.grab_json_response( - '/api/dev/{0}/outlet/{1}'.format(pdu, outlet), - {'cmd': 'control', 'token': self.token, - 'data': {'action': state, 'delay': False}}) + "/api/dev/{0}/outlet/{1}".format(pdu, outlet), + { + "cmd": "control", + "token": self.token, + "data": {"action": state, "delay": False}, + }, + ) -def process_measurement(keyname, name, enttype, entname, measurement, readings, category): - if measurement['type'] == 'realPower': - if category not in ('all', 'power'): + +def process_measurement( + keyname, name, enttype, entname, measurement, readings, category +): + if measurement["type"] == "realPower": + if category not in ("all", "power"): return - readtype = 'Real Power' - elif measurement['type'] == 'apparentPower': - if category not in ('all', 'power'): + readtype = "Real Power" + elif measurement["type"] == "apparentPower": + if category not in ("all", "power"): return - readtype = 'Apparent Power' - elif measurement['type'] == 'energy': - if category not in ('all', 'energy'): + readtype = "Apparent Power" + elif measurement["type"] == "energy": + if category not in ("all", "energy"): return - readtype = 'Energy' - elif measurement['type'] == 'voltage': - if category not in ('all',): + readtype = "Energy" + elif measurement["type"] == "voltage": + if category not in ("all",): return - readtype = 'Voltage' - elif measurement['type'] == 'temperature': - readtype = 'Temperature' - elif measurement['type'] == 'dewpoint': - readtype = 'Dewpoint' - elif measurement['type'] == 'humidity': - readtype = 'Humidity' + readtype = "Voltage" + elif measurement["type"] == "temperature": + readtype = "Temperature" + elif measurement["type"] == "dewpoint": + readtype = "Dewpoint" + elif measurement["type"] == "humidity": + readtype = "Humidity" else: return - myname = entname + ' ' + readtype - if name != 'all' and simplify_name(myname) != name: + myname = entname + " " + readtype + if name != "all" and simplify_name(myname) != name: return - readings.append({ - 'name': myname, - 'value': float(measurement['value']), - 'units': measurement['units'], - 'type': readtype.split()[-1] - }) - + readings.append( + { + "name": myname, + "value": float(measurement["value"]), + "units": measurement["units"], + "type": readtype.split()[-1], + } + ) + def process_measurements(name, category, measurements, enttype, readings): for measure in util.natural_sort(list(measurements)): - measurement = measurements[measure]['measurement'] - entname = measurements[measure]['name'] + measurement = measurements[measure]["measurement"] + entname = measurements[measure]["name"] for measureid in measurement: - process_measurement(measure, name, enttype, entname, measurement[measureid], readings, category) - + process_measurement( + measure, + name, + enttype, + entname, + measurement[measureid], + readings, + category, + ) + _sensors_by_node = {} + + def read_sensors(element, node, configmanager): category, name = element[-2:] justnames = False if len(element) == 3: # just get names category = name - name = 'all' + name = "all" justnames = True - if category in ('leds, fans', 'temperature'): + if category in ("leds, fans", "temperature"): return sn = _sensors_by_node.get(node, None) if not sn or sn[1] < time.time(): gc = GeistClient(node, configmanager) - adev = gc.wc.grab_json_response('/api/dev') + adev = gc.wc.grab_json_response("/api/dev") _sensors_by_node[node] = (adev, time.time() + 1) sn = _sensors_by_node.get(node, None) - dbt = data_by_type(sn[0]['data']) + dbt = data_by_type(sn[0]["data"]) readings = [] - for datatype in dbt: + for datatype in dbt: datum = dbt[datatype] - process_measurements(name, category, datum['entity'], 'entity', readings) - if 'outlet' in datum: - process_measurements(name, category, datum['outlet'], 'outlet', readings) + process_measurements(name, category, datum["entity"], "entity", readings) + if "outlet" in datum: + process_measurements(name, category, datum["outlet"], "outlet", readings) if justnames: for reading in readings: - yield msg.ChildCollection(simplify_name(reading['name'])) + yield msg.ChildCollection(simplify_name(reading["name"])) else: yield msg.SensorReadings(readings, name=node) + def get_outlet(node, configmanager, element): gc = GeistClient(node, configmanager) state = gc.get_outlet(element[-1]) return msg.PowerState(node=node, state=state) + def read_firmware(node, configmanager): gc = GeistClient(node, configmanager) - adev = gc.wc.grab_json_response('/api/sys') - myversion = adev['data']['version'] - yield msg.Firmware([{'PDU Firmware': {'version': myversion}}], node) + adev = gc.wc.grab_json_response("/api/sys") + myversion = adev["data"]["version"] + yield msg.Firmware([{"PDU Firmware": {"version": myversion}}], node) + + +def read_inventory(element, node, configmanager): + _inventory = {} + inventory = {} + gc = GeistClient(node, configmanager) + adev = gc.wc.grab_json_response("/api/sys") + basedata = adev["data"] + inventory["present"] = True + inventory["name"] = "PDU" + for elem in basedata.items(): + if ( + elem[0] != "component" + and elem[0] != "locale" + and elem[0] != "state" + and elem[0] != "contact" + ): # and elem[0] !='name': + _inventory[elem[0]] = elem[1] + elif elem[0] == "component": + tempname = "" + for component in basedata["component"].items(): + for item in component: + if type(item) == str: + tempname = item + else: + for entry in item.items(): + _inventory[tempname + " " + entry[0]] = entry[1] + + inventory["information"] = _inventory + + yield msg.KeyValueData({"inventory": [inventory]}, node) + def retrieve(nodes, element, configmanager, inputdata): - if 'outlets' in element: + if "outlets" in element: gp = greenpool.GreenPile(pdupool) for node in nodes: - gp.spawn(get_outlet, node, configmanager, element) + gp.spawn(get_outlet, element, node, configmanager) for res in gp: yield res - + return - elif element[0] == 'sensors': + elif element[0] == "sensors": gp = greenpool.GreenPile(pdupool) for node in nodes: gp.spawn(read_sensors, element, node, configmanager) @@ -232,21 +291,30 @@ def retrieve(nodes, element, configmanager, inputdata): for datum in rsp: yield datum return - elif '/'.join(element).startswith('inventory/firmware/all'): + elif "/".join(element).startswith("inventory/firmware/all"): gp = greenpool.GreenPile(pdupool) for node in nodes: gp.spawn(read_firmware, node, configmanager) for rsp in gp: for datum in rsp: yield datum + + elif "/".join(element).startswith("inventory/hardware/all"): + gp = greenpool.GreenPile(pdupool) + for node in nodes: + gp.spawn(read_inventory, element, node, configmanager) + for rsp in gp: + for datum in rsp: + yield datum else: for node in nodes: - yield msg.ConfluentResourceUnavailable(node, 'Not implemented') + yield msg.ConfluentResourceUnavailable(node, "Not implemented") return - + + def update(nodes, element, configmanager, inputdata): - if 'outlets' not in element: - yield msg.ConfluentResourceUnavailable(node, 'Not implemented') + if "outlets" not in element: + yield msg.ConfluentResourceUnavailable(node, "Not implemented") return for node in nodes: gc = GeistClient(node, configmanager) From c95052438688cf9481ae9f18612340ed23325393 Mon Sep 17 00:00:00 2001 From: weragrzeda Date: Thu, 7 Sep 2023 13:08:33 +0200 Subject: [PATCH 004/146] geist.py outlet state fix --- .../plugins/hardwaremanagement/geist.py | 264 ++++++++---------- 1 file changed, 119 insertions(+), 145 deletions(-) diff --git a/confluent_server/confluent/plugins/hardwaremanagement/geist.py b/confluent_server/confluent/plugins/hardwaremanagement/geist.py index 64bf04f3..620a0576 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/geist.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/geist.py @@ -19,29 +19,25 @@ import confluent.exceptions as exc import eventlet.green.time as time import eventlet import eventlet.greenpool as greenpool - - +import json def simplify_name(name): - return name.lower().replace(" ", "_").replace("/", "-").replace("_-_", "-") - + return name.lower().replace(' ', '_').replace('/', '-').replace( + '_-_', '-') pdupool = greenpool.GreenPool(128) - def data_by_type(indata): databytype = {} for keyname in indata: obj = indata[keyname] - objtype = obj.get("type", None) + objtype = obj.get('type', None) if not objtype: continue if objtype in databytype: - raise Exception( - "Multiple instances of type {} not yet supported".format(objtype) - ) + raise Exception("Multiple instances of type {} not yet supported".format(objtype)) databytype[objtype] = obj - obj["keyname"] = keyname + obj['keyname'] = keyname return databytype @@ -63,227 +59,206 @@ class GeistClient(object): def wc(self): if self._wc: return self._wc - targcfg = self.configmanager.get_node_attributes( - self.node, ["hardwaremanagement.manager"], decrypt=True - ) + targcfg = self.configmanager.get_node_attributes(self.node, + ['hardwaremanagement.manager'], + decrypt=True) targcfg = targcfg.get(self.node, {}) - target = targcfg.get("hardwaremanagement.manager", {}).get("value", None) + target = targcfg.get( + 'hardwaremanagement.manager', {}).get('value', None) if not target: target = self.node - target = target.split("/", 1)[0] + target = target.split('/', 1)[0] cv = util.TLSCertVerifier( - self.configmanager, self.node, "pubkeys.tls_hardwaremanager" - ).verify_cert + self.configmanager, self.node, + 'pubkeys.tls_hardwaremanager').verify_cert self._wc = wc.SecureHTTPConnection(target, port=443, verifycallback=cv) return self._wc def login(self, configmanager): - credcfg = configmanager.get_node_attributes( - self.node, - ["secret.hardwaremanagementuser", "secret.hardwaremanagementpassword"], - decrypt=True, - ) + credcfg = configmanager.get_node_attributes(self.node, + ['secret.hardwaremanagementuser', + 'secret.hardwaremanagementpassword'], + decrypt=True) credcfg = credcfg.get(self.node, {}) - username = credcfg.get("secret.hardwaremanagementuser", {}).get("value", None) - passwd = credcfg.get("secret.hardwaremanagementpassword", {}).get("value", None) + username = credcfg.get( + 'secret.hardwaremanagementuser', {}).get('value', None) + passwd = credcfg.get( + 'secret.hardwaremanagementpassword', {}).get('value', None) if not isinstance(username, str): - username = username.decode("utf8") + username = username.decode('utf8') if not isinstance(passwd, str): - passwd = passwd.decode("utf8") + passwd = passwd.decode('utf8') if not username or not passwd: - raise Exception("Missing username or password") + raise Exception('Missing username or password') self.username = username rsp = self.wc.grab_json_response( - "/api/auth/{0}".format(username), - {"cmd": "login", "data": {"password": passwd}}, - ) - token = rsp["data"]["token"] + '/api/auth/{0}'.format(username), + {'cmd': 'login', 'data': {'password': passwd}}) + token = rsp['data']['token'] return token def logout(self): if self._token: - self.wc.grab_json_response( - "/api/auth/{0}".format(self.username), - {"cmd": "logout", "token": self.token}, - ) + self.wc.grab_json_response('/api/auth/{0}'.format(self.username), + {'cmd': 'logout', 'token': self.token}) self._token = None def get_outlet(self, outlet): - rsp = self.wc.grab_json_response("/api/dev") - rsp = rsp["data"] + rsp = self.wc.grab_json_response('/api/dev') + rsp = rsp['data'] dbt = data_by_type(rsp) - if "t3hd" in dbt: - del dbt["t3hd"] + + if 't3hd' in dbt: + del dbt['t3hd'] if len(dbt) != 1: - raise Exception("Multiple PDUs not supported per pdu") + raise Exception('Multiple PDUs not supported per pdu') pdutype = list(dbt)[0] - outlet = dbt[pdutype]["outlet"][str(int(outlet) - 1)] - state = outlet["state"].split("2")[-1] + + outlet = dbt[pdutype]['outlet'][ str(int(outlet[0]) - 1) ] + + state = outlet['state'].split('2')[-1] return state def set_outlet(self, outlet, state): - rsp = self.wc.grab_json_response("/api/dev") - dbt = data_by_type(rsp["data"]) - if "t3hd" in dbt: - del dbt["t3hd"] + rsp = self.wc.grab_json_response('/api/dev') + dbt = data_by_type(rsp['data']) + if 't3hd' in dbt: + del dbt['t3hd'] if len(dbt) != 1: self.logout() - raise Exception("Multiple PDUs per endpoint not supported") - pdu = dbt[list(dbt)[0]]["keyname"] + raise Exception('Multiple PDUs per endpoint not supported') + pdu = dbt[list(dbt)[0]]['keyname'] outlet = int(outlet) - 1 rsp = self.wc.grab_json_response( - "/api/dev/{0}/outlet/{1}".format(pdu, outlet), - { - "cmd": "control", - "token": self.token, - "data": {"action": state, "delay": False}, - }, - ) + '/api/dev/{0}/outlet/{1}'.format(pdu, outlet), + {'cmd': 'control', 'token': self.token, + 'data': {'action': state, 'delay': False}}) - -def process_measurement( - keyname, name, enttype, entname, measurement, readings, category -): - if measurement["type"] == "realPower": - if category not in ("all", "power"): +def process_measurement(keyname, name, enttype, entname, measurement, readings, category): + if measurement['type'] == 'realPower': + if category not in ('all', 'power'): return - readtype = "Real Power" - elif measurement["type"] == "apparentPower": - if category not in ("all", "power"): + readtype = 'Real Power' + elif measurement['type'] == 'apparentPower': + if category not in ('all', 'power'): return - readtype = "Apparent Power" - elif measurement["type"] == "energy": - if category not in ("all", "energy"): + readtype = 'Apparent Power' + elif measurement['type'] == 'energy': + if category not in ('all', 'energy'): return - readtype = "Energy" - elif measurement["type"] == "voltage": - if category not in ("all",): + readtype = 'Energy' + elif measurement['type'] == 'voltage': + if category not in ('all',): return - readtype = "Voltage" - elif measurement["type"] == "temperature": - readtype = "Temperature" - elif measurement["type"] == "dewpoint": - readtype = "Dewpoint" - elif measurement["type"] == "humidity": - readtype = "Humidity" + readtype = 'Voltage' + elif measurement['type'] == 'temperature': + readtype = 'Temperature' + elif measurement['type'] == 'dewpoint': + readtype = 'Dewpoint' + elif measurement['type'] == 'humidity': + readtype = 'Humidity' else: return - myname = entname + " " + readtype - if name != "all" and simplify_name(myname) != name: + myname = entname + ' ' + readtype + if name != 'all' and simplify_name(myname) != name: return - readings.append( - { - "name": myname, - "value": float(measurement["value"]), - "units": measurement["units"], - "type": readtype.split()[-1], - } - ) - + readings.append({ + 'name': myname, + 'value': float(measurement['value']), + 'units': measurement['units'], + 'type': readtype.split()[-1] + }) + def process_measurements(name, category, measurements, enttype, readings): for measure in util.natural_sort(list(measurements)): - measurement = measurements[measure]["measurement"] - entname = measurements[measure]["name"] + measurement = measurements[measure]['measurement'] + entname = measurements[measure]['name'] for measureid in measurement: - process_measurement( - measure, - name, - enttype, - entname, - measurement[measureid], - readings, - category, - ) - + process_measurement(measure, name, enttype, entname, measurement[measureid], readings, category) + _sensors_by_node = {} - - def read_sensors(element, node, configmanager): category, name = element[-2:] justnames = False if len(element) == 3: # just get names category = name - name = "all" + name = 'all' justnames = True - if category in ("leds, fans", "temperature"): + if category in ('leds, fans', 'temperature'): return sn = _sensors_by_node.get(node, None) if not sn or sn[1] < time.time(): gc = GeistClient(node, configmanager) - adev = gc.wc.grab_json_response("/api/dev") + adev = gc.wc.grab_json_response('/api/dev') _sensors_by_node[node] = (adev, time.time() + 1) sn = _sensors_by_node.get(node, None) - dbt = data_by_type(sn[0]["data"]) + dbt = data_by_type(sn[0]['data']) readings = [] - for datatype in dbt: + for datatype in dbt: datum = dbt[datatype] - process_measurements(name, category, datum["entity"], "entity", readings) - if "outlet" in datum: - process_measurements(name, category, datum["outlet"], "outlet", readings) + process_measurements(name, category, datum['entity'], 'entity', readings) + if 'outlet' in datum: + process_measurements(name, category, datum['outlet'], 'outlet', readings) if justnames: for reading in readings: - yield msg.ChildCollection(simplify_name(reading["name"])) + yield msg.ChildCollection(simplify_name(reading['name'])) else: yield msg.SensorReadings(readings, name=node) - -def get_outlet(node, configmanager, element): +def get_outlet(element, node, configmanager): gc = GeistClient(node, configmanager) state = gc.get_outlet(element[-1]) - return msg.PowerState(node=node, state=state) + return msg.PowerState(node=node, state=state) def read_firmware(node, configmanager): gc = GeistClient(node, configmanager) - adev = gc.wc.grab_json_response("/api/sys") - myversion = adev["data"]["version"] - yield msg.Firmware([{"PDU Firmware": {"version": myversion}}], node) + adev = gc.wc.grab_json_response('/api/sys') + myversion = adev['data']['version'] + yield msg.Firmware([{'PDU Firmware': {'version': myversion}}], node) def read_inventory(element, node, configmanager): _inventory = {} inventory = {} gc = GeistClient(node, configmanager) - adev = gc.wc.grab_json_response("/api/sys") - basedata = adev["data"] - inventory["present"] = True - inventory["name"] = "PDU" + adev = gc.wc.grab_json_response('/api/sys') + basedata = adev['data'] + inventory['present'] = True + inventory['name'] = 'PDU' for elem in basedata.items(): - if ( - elem[0] != "component" - and elem[0] != "locale" - and elem[0] != "state" - and elem[0] != "contact" - ): # and elem[0] !='name': - _inventory[elem[0]] = elem[1] - elif elem[0] == "component": - tempname = "" - for component in basedata["component"].items(): + if elem[0] !='component' and elem[0] !='locale' and elem[0] !='state' and elem[0] !='contact': # and elem[0] !='name': + _inventory[elem[0]] = elem[1] + elif elem[0] =='component': + tempname = '' + for component in basedata['component'].items(): for item in component: if type(item) == str: tempname = item else: for entry in item.items(): - _inventory[tempname + " " + entry[0]] = entry[1] + _inventory[tempname + ' ' + entry[0]] = entry[1] + - inventory["information"] = _inventory - - yield msg.KeyValueData({"inventory": [inventory]}, node) + inventory['information']= _inventory + yield msg.KeyValueData({'inventory': [inventory]}, node) + def retrieve(nodes, element, configmanager, inputdata): - if "outlets" in element: + if 'outlets' in element: gp = greenpool.GreenPile(pdupool) for node in nodes: + print(element) gp.spawn(get_outlet, element, node, configmanager) for res in gp: yield res - + return - elif element[0] == "sensors": + elif element[0] == 'sensors': gp = greenpool.GreenPile(pdupool) for node in nodes: gp.spawn(read_sensors, element, node, configmanager) @@ -291,7 +266,7 @@ def retrieve(nodes, element, configmanager, inputdata): for datum in rsp: yield datum return - elif "/".join(element).startswith("inventory/firmware/all"): + elif '/'.join(element).startswith('inventory/firmware/all'): gp = greenpool.GreenPile(pdupool) for node in nodes: gp.spawn(read_firmware, node, configmanager) @@ -299,7 +274,7 @@ def retrieve(nodes, element, configmanager, inputdata): for datum in rsp: yield datum - elif "/".join(element).startswith("inventory/hardware/all"): + elif '/'.join(element).startswith('inventory/hardware/all'): gp = greenpool.GreenPile(pdupool) for node in nodes: gp.spawn(read_inventory, element, node, configmanager) @@ -308,13 +283,12 @@ def retrieve(nodes, element, configmanager, inputdata): yield datum else: for node in nodes: - yield msg.ConfluentResourceUnavailable(node, "Not implemented") + yield msg.ConfluentResourceUnavailable(node, 'Not implemented') return - - + def update(nodes, element, configmanager, inputdata): - if "outlets" not in element: - yield msg.ConfluentResourceUnavailable(node, "Not implemented") + if 'outlets' not in element: + yield msg.ConfluentResourceUnavailable(node, 'Not implemented') return for node in nodes: gc = GeistClient(node, configmanager) From 7b19e67bf0daeb87a0b526a0acf636c7e50bcd81 Mon Sep 17 00:00:00 2001 From: weragrzeda Date: Thu, 21 Sep 2023 06:27:12 +0200 Subject: [PATCH 005/146] amperage and outlet fix for geist.py --- .../plugins/hardwaremanagement/geist.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/confluent_server/confluent/plugins/hardwaremanagement/geist.py b/confluent_server/confluent/plugins/hardwaremanagement/geist.py index 620a0576..a628eb34 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/geist.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/geist.py @@ -84,6 +84,7 @@ class GeistClient(object): 'secret.hardwaremanagementuser', {}).get('value', None) passwd = credcfg.get( 'secret.hardwaremanagementpassword', {}).get('value', None) + if not isinstance(username, str): username = username.decode('utf8') if not isinstance(passwd, str): @@ -94,6 +95,7 @@ class GeistClient(object): rsp = self.wc.grab_json_response( '/api/auth/{0}'.format(username), {'cmd': 'login', 'data': {'password': passwd}}) + token = rsp['data']['token'] return token @@ -113,9 +115,7 @@ class GeistClient(object): if len(dbt) != 1: raise Exception('Multiple PDUs not supported per pdu') pdutype = list(dbt)[0] - - outlet = dbt[pdutype]['outlet'][ str(int(outlet[0]) - 1) ] - + outlet = dbt[pdutype]['outlet'][ str(int(outlet.join(c for c in outlet if c.isdigit())) - 1) ] state = outlet['state'].split('2')[-1] return state @@ -128,7 +128,8 @@ class GeistClient(object): self.logout() raise Exception('Multiple PDUs per endpoint not supported') pdu = dbt[list(dbt)[0]]['keyname'] - outlet = int(outlet) - 1 + outlet = int(outlet.join(c for c in outlet if c.isdigit())) - 1 + rsp = self.wc.grab_json_response( '/api/dev/{0}/outlet/{1}'.format(pdu, outlet), {'cmd': 'control', 'token': self.token, @@ -151,6 +152,10 @@ def process_measurement(keyname, name, enttype, entname, measurement, readings, if category not in ('all',): return readtype = 'Voltage' + elif measurement['type'] == 'current': + if category not in ('all',): + return + readtype = 'Current' elif measurement['type'] == 'temperature': readtype = 'Temperature' elif measurement['type'] == 'dewpoint': @@ -196,10 +201,12 @@ def read_sensors(element, node, configmanager): _sensors_by_node[node] = (adev, time.time() + 1) sn = _sensors_by_node.get(node, None) dbt = data_by_type(sn[0]['data']) + readings = [] for datatype in dbt: datum = dbt[datatype] process_measurements(name, category, datum['entity'], 'entity', readings) + if 'outlet' in datum: process_measurements(name, category, datum['outlet'], 'outlet', readings) if justnames: @@ -249,10 +256,11 @@ def read_inventory(element, node, configmanager): def retrieve(nodes, element, configmanager, inputdata): + if 'outlets' in element: gp = greenpool.GreenPile(pdupool) for node in nodes: - print(element) + gp.spawn(get_outlet, element, node, configmanager) for res in gp: yield res From 820d255a1ab7a08aca2799460946365736fc1de4 Mon Sep 17 00:00:00 2001 From: tkucherera Date: Mon, 2 Oct 2023 11:23:23 -0400 Subject: [PATCH 006/146] staging feature --- confluent_server/confluent/core.py | 86 +++++++++++++++++++++++++- confluent_server/confluent/httpapi.py | 39 ++++++++++++ confluent_server/confluent/messages.py | 12 ++++ 3 files changed, 136 insertions(+), 1 deletion(-) diff --git a/confluent_server/confluent/core.py b/confluent_server/confluent/core.py index a9ee1dba..e94d2abb 100644 --- a/confluent_server/confluent/core.py +++ b/confluent_server/confluent/core.py @@ -69,6 +69,7 @@ import os import eventlet.green.socket as socket import struct import sys +import uuid pluginmap = {} dispatch_plugins = (b'ipmi', u'ipmi', b'redfish', u'redfish', b'tsmsol', u'tsmsol', b'geist', u'geist', b'deltapdu', u'deltapdu', b'eatonpdu', u'eatonpdu', b'affluent', u'affluent', b'cnos', u'cnos') @@ -160,7 +161,7 @@ def _merge_dict(original, custom): rootcollections = ['deployment/', 'discovery/', 'events/', 'networking/', 'noderange/', 'nodes/', 'nodegroups/', 'usergroups/' , - 'users/', 'uuid', 'version'] + 'users/', 'uuid', 'version', 'staging/'] class PluginRoute(object): @@ -1211,6 +1212,87 @@ def handle_discovery(pathcomponents, operation, configmanager, inputdata): if pathcomponents[0] == 'detected': pass +class Staging: + def __init__(self, user, uuid): + self.uuid_str = uuid + self.storage_folder = '/var/lib/confluent/client_assets/' + self.uuid_str + self.filename = None + self.user = user + self.base_folder = os.path.exists('/var/lib/confluent/client_assets/') + + if not self.base_folder: + try: + os.mkdir('/var/lib/confluent/client_assets/') + except Exception as e: + raise OSError(str(e)) + + def getUUID(self): + return self.uuid_str + + def get_push_url(self): + return 'staging/{0}/{1}'.format(self.user,self.uuid_str) + + def create_directory(self): + try: + os.mkdir(self.storage_folder) + return True + except OSError as e: + raise exc.InvalidArgumentException(str(e)) + + def get_file_name(self): + stage_file = '{}/filename.txt'.format(self.storage_folder) + try: + with open(stage_file, 'r') as f: + filename = f.readline() + os.remove(stage_file) + return self.storage_folder + '/{}'.format(filename) + except FileNotFoundError: + file = None + return False + + def deldirectory(self): + pass + +def handle_staging(pathcomponents, operation, configmanager, inputdata): + ''' + e.g push_url: /confluent-api/staging/user/ + ''' + if operation == 'create': + if len(pathcomponents) == 1: + stage = Staging(inputdata['user'],str(uuid.uuid1())) + if stage.create_directory(): + if 'filename' in inputdata: + data_file = stage.storage_folder + '/filename.txt' + with open(data_file, 'w') as f: + f.write(inputdata['filename']) + else: + raise Exception('Error: Missing filename arg') + push_url = stage.get_push_url() + yield msg.CreatedResource(push_url) + + elif len(pathcomponents) == 3: + stage = Staging(pathcomponents[1], pathcomponents[2]) + file = stage.get_file_name() + if 'filedata' in inputdata and file: + content_length = inputdata['content_length'] + remaining_length = content_length + filedata = inputdata['filedata'] + chunk_size = 16384 + progress = 0.0 + with open(file, 'wb') as f: + while remaining_length > 0: + progress = (1 - (remaining_length/content_length)) * 100 + datachunk = filedata['wsgi.input'].read(min(chunk_size, remaining_length)) + f.write(datachunk) + remaining_length -= len(datachunk) + yield msg.FileUploadProgress(progress) + yield msg.FileUploadProgress(100) + + + elif operation == 'retrieve': + pass + return + def handle_path(path, operation, configmanager, inputdata=None, autostrip=True): """Given a full path request, return an object. @@ -1316,5 +1398,7 @@ def handle_path(path, operation, configmanager, inputdata=None, autostrip=True): elif pathcomponents[0] == 'discovery': return handle_discovery(pathcomponents[1:], operation, configmanager, inputdata) + elif pathcomponents[0] == 'staging': + return handle_staging(pathcomponents, operation, configmanager, inputdata) else: raise exc.NotFoundException() diff --git a/confluent_server/confluent/httpapi.py b/confluent_server/confluent/httpapi.py index 5a145a0c..baf40606 100644 --- a/confluent_server/confluent/httpapi.py +++ b/confluent_server/confluent/httpapi.py @@ -887,6 +887,45 @@ def resourcehandler_backend(env, start_response): start_response('200 OK', headers) yield rsp return + + elif (operation == 'create' and ('/staging' in env['PATH_INFO'])): + url = env['PATH_INFO'] + args_dict = {} + content_length = int(env.get('CONTENT_LENGTH', 0)) + if content_length > 0 and (len(url.split('/')) > 2): + # check if the user and the url defined user are the same + if authorized['username'] == url.split('/')[2]: + args_dict.update({'filedata':env, 'content_length': content_length}) + hdlr = pluginapi.handle_path(url, operation, cfgmgr, args_dict) + for resp in hdlr: + if isinstance(resp, confluent.messages.FileUploadProgress): + if resp.kvpairs['progress']['value'] == 100: + progress = resp.kvpairs['progress']['value'] + start_response('200 OK', headers) + yield json.dumps({'data': 'done'}) + return + else: + start_response('401 Unauthorized', headers) + yield json.dumps({'data': 'You do not have permission to write to file'}) + return + elif 'application/json' in reqtype and (len(url.split('/')) == 2): + if not isinstance(reqbody, str): + reqbody = reqbody.decode('utf8') + pbody = json.loads(reqbody) + args = pbody['args'] + args_dict.update({'filename': args, 'user': authorized['username']}) + try: + args_dict.update({'bank': pbody['bank']}) + except KeyError: + pass + hdlr = pluginapi.handle_path(url, operation, cfgmgr, args_dict) + for res in hdlr: + if isinstance(res, confluent.messages.CreatedResource): + stageurl = res.kvpairs['created'] + start_response('200 OK', headers) + yield json.dumps({'data': stageurl}) + return + else: # normal request url = env['PATH_INFO'] diff --git a/confluent_server/confluent/messages.py b/confluent_server/confluent/messages.py index a24a4d78..d0e720be 100644 --- a/confluent_server/confluent/messages.py +++ b/confluent_server/confluent/messages.py @@ -622,6 +622,18 @@ class SavedFile(ConfluentMessage): self.myargs = (node, file) self.kvpairs = {node: {'filename': file}} +class FileUploadProgress(ConfluentMessage): + readonly = True + + def __init__(self, progress, name=None): + self.myargs = (progress) + self.stripped = False + self.notnode = name is None + if self.notnode: + self.kvpairs = {'progress': {'value': progress}} + else: + self.kvpairs = {name: {'progress': {'value': progress}}} + class InputAlertData(ConfluentMessage): def __init__(self, path, inputdata, nodes=None): From ac68f1f22c90a7852459996bd87e07804900d90e Mon Sep 17 00:00:00 2001 From: Wera Grzeda Date: Fri, 20 Oct 2023 11:38:11 +0200 Subject: [PATCH 007/146] new eaton pdu power readings --- .../plugins/hardwaremanagement/eatonpdu.py | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/confluent_server/confluent/plugins/hardwaremanagement/eatonpdu.py b/confluent_server/confluent/plugins/hardwaremanagement/eatonpdu.py index da60b182..9b5ade5b 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/eatonpdu.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/eatonpdu.py @@ -156,6 +156,7 @@ def get_sensor_data(element, node, configmanager): gc.logout() _sensors_by_node[node] = [sdata, time.time() + 1] sn = _sensors_by_node.get(node, None) +# print(sn) for outlet in sn[0]: for sensename in sn[0][outlet]: myname = '{0} {1}'.format(outlet, sensename) @@ -277,22 +278,35 @@ class PDUClient(object): if outdata[0] == outlet: return 'on' if outdata[3] else 'off' return + def get_outlet_sensors(self): + rsp = self.do_request('cgi_pdu_outlets') + data = sanitize_json(rsp[0]) + data = json.loads(data) + data = data['data'][0] + + + return data def get_sensor_data(self): rsp = self.do_request1('cgi_overview') - + data = sanitize_json(rsp[0]) data = json.loads(data) + data1 = data['data'][4][0][8] - data = data['data'][0] - sdata = {} - for outdata in data: + data = self.get_outlet_sensors() + sdata = {} + + for outdata in data: + outsense = {} - outletname = outdata[3] + outletname = outdata[0][1] + + outsense['Power'] = { - 'value': outdata[5], - 'units': 'kW', + 'value': outdata[4], + 'units': 'W', 'type': 'Power', } sdata[outletname] = outsense From 62ab361ef80eba4ee3d0930f1bb0b40769047b71 Mon Sep 17 00:00:00 2001 From: Wera Grzeda Date: Mon, 23 Oct 2023 08:16:27 +0200 Subject: [PATCH 008/146] nicer output for inventory --- .../plugins/hardwaremanagement/geist.py | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/confluent_server/confluent/plugins/hardwaremanagement/geist.py b/confluent_server/confluent/plugins/hardwaremanagement/geist.py index a628eb34..06af8675 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/geist.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/geist.py @@ -115,7 +115,8 @@ class GeistClient(object): if len(dbt) != 1: raise Exception('Multiple PDUs not supported per pdu') pdutype = list(dbt)[0] - outlet = dbt[pdutype]['outlet'][ str(int(outlet.join(c for c in outlet if c.isdigit())) - 1) ] + outlet = dbt[pdutype]['outlet'][ str(int(outlet)- 1) ] +# print(outlet) state = outlet['state'].split('2')[-1] return state @@ -128,7 +129,7 @@ class GeistClient(object): self.logout() raise Exception('Multiple PDUs per endpoint not supported') pdu = dbt[list(dbt)[0]]['keyname'] - outlet = int(outlet.join(c for c in outlet if c.isdigit())) - 1 + outlet = int(outlet) - 1 rsp = self.wc.grab_json_response( '/api/dev/{0}/outlet/{1}'.format(pdu, outlet), @@ -237,17 +238,29 @@ def read_inventory(element, node, configmanager): inventory['present'] = True inventory['name'] = 'PDU' for elem in basedata.items(): - if elem[0] !='component' and elem[0] !='locale' and elem[0] !='state' and elem[0] !='contact': # and elem[0] !='name': - _inventory[elem[0]] = elem[1] + + if elem[0] !='component' and elem[0] !='locale' and elem[0] !='state' and elem[0] !='contact' and elem[0] !='appVersion' and elem[0] !='build' and elem[0] !='version' and elem[0] !='apiVersion': + temp = elem[0] + if elem[0] == "serialNumber": + temp = "Serial" + elif elem[0] == "partNumber": + temp = "P/N" + elif elem[0] == "modelNumber": + temp= "Lenovo P/N and Serial" + _inventory[temp] = elem[1] elif elem[0] =='component': tempname = '' for component in basedata['component'].items(): for item in component: if type(item) == str: + tempname = item else: for entry in item.items(): - _inventory[tempname + ' ' + entry[0]] = entry[1] + temp = entry[0] + if temp == 'sn': + temp = "Serial" + _inventory[tempname + ' ' + temp] = entry[1] inventory['information']= _inventory From 9954b227a95b33c53c2869e29c091ee68f81cfc9 Mon Sep 17 00:00:00 2001 From: Wera Grzeda Date: Tue, 23 Apr 2024 15:06:28 +0200 Subject: [PATCH 009/146] Revert "nodeconfig ntp servers for smms" This reverts commit 44405baaf39af36d1b695d1f8be6836319257816. --- confluent_client/bin/nodeconfig | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/confluent_client/bin/nodeconfig b/confluent_client/bin/nodeconfig index 0ba8d351..06d512c7 100755 --- a/confluent_client/bin/nodeconfig +++ b/confluent_client/bin/nodeconfig @@ -1,4 +1,4 @@ -#!/usr/libexec/platform-python +#!/usr/bin/python2 # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2017 Lenovo @@ -98,17 +98,8 @@ cfgpaths = { 'static_v6_gateway'), 'bmc.hostname': ( 'configuration/management_controller/hostname', 'hostname'), - 'bmc.ntp': ( - 'configuration/management_controller/ntp/enabled', 'state'), - 'bmc.ntpServer1': ( - 'configuration/management_controller/ntp/servers/1', 'server'), - 'bmc.ntpServer2': ( - 'configuration/management_controller/ntp/servers/2', 'server'), - 'bmc.ntpServer3': ( - 'configuration/management_controller/ntp/servers/3', 'server') } - autodeps = { 'bmc.ipv4_address': (('bmc.ipv4_method', 'static'),) } @@ -122,7 +113,6 @@ client.check_globbing(noderange) setmode = None assignment = {} queryparms = {} - printsys = [] printbmc = [] printextbmc = [] @@ -141,6 +131,7 @@ if len(args) == 1 or options.exclude: queryparms[path] = {} queryparms[path][attrib] = candidate + def _assign_value(): if key not in cfgpaths: setsys[key] = value @@ -242,7 +233,6 @@ else: parse_config_line(args[1:]) session = client.Command() rcode = 0 - if options.restoredefault: session.stop_if_noderange_over(noderange, options.maxnodes) if options.restoredefault.lower() in ( @@ -305,10 +295,8 @@ else: for path in queryparms: if options.comparedefault: continue - - rcode |= client.print_attrib_path(path, session, list(queryparms[path]),NullOpt(), queryparms[path]) - - + rcode |= client.print_attrib_path(path, session, list(queryparms[path]), + NullOpt(), queryparms[path]) if printsys == 'all' or printextbmc or printbmc or printallbmc: if printbmc or not printextbmc: rcode |= client.print_attrib_path( From 3b69c542034a7311942537c5398c2ac334e88d4e Mon Sep 17 00:00:00 2001 From: Wera Grzeda Date: Tue, 23 Apr 2024 15:08:36 +0200 Subject: [PATCH 010/146] Revert "new eaton pdu power readings" This reverts commit ac68f1f22c90a7852459996bd87e07804900d90e. --- .../plugins/hardwaremanagement/eatonpdu.py | 26 +++++-------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/confluent_server/confluent/plugins/hardwaremanagement/eatonpdu.py b/confluent_server/confluent/plugins/hardwaremanagement/eatonpdu.py index 9b5ade5b..da60b182 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/eatonpdu.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/eatonpdu.py @@ -156,7 +156,6 @@ def get_sensor_data(element, node, configmanager): gc.logout() _sensors_by_node[node] = [sdata, time.time() + 1] sn = _sensors_by_node.get(node, None) -# print(sn) for outlet in sn[0]: for sensename in sn[0][outlet]: myname = '{0} {1}'.format(outlet, sensename) @@ -278,35 +277,22 @@ class PDUClient(object): if outdata[0] == outlet: return 'on' if outdata[3] else 'off' return - def get_outlet_sensors(self): - rsp = self.do_request('cgi_pdu_outlets') - data = sanitize_json(rsp[0]) - data = json.loads(data) - data = data['data'][0] - - - return data def get_sensor_data(self): rsp = self.do_request1('cgi_overview') - + data = sanitize_json(rsp[0]) data = json.loads(data) - data1 = data['data'][4][0][8] - - data = self.get_outlet_sensors() + data = data['data'][0] sdata = {} - for outdata in data: - + outsense = {} - outletname = outdata[0][1] - - + outletname = outdata[3] outsense['Power'] = { - 'value': outdata[4], - 'units': 'W', + 'value': outdata[5], + 'units': 'kW', 'type': 'Power', } sdata[outletname] = outsense From 21b8534fefaaee5ebee23247da6dbacf94d3f506 Mon Sep 17 00:00:00 2001 From: Wera Grzeda Date: Tue, 23 Apr 2024 15:29:35 +0200 Subject: [PATCH 011/146] Revert "my changes to Eaton PDU sensors" This reverts commit 6d87d11f5e6decd854995d4b5bd7188c049c418a. --- .../plugins/hardwaremanagement/eatonpdu.py | 49 +++++-------------- 1 file changed, 11 insertions(+), 38 deletions(-) diff --git a/confluent_server/confluent/plugins/hardwaremanagement/eatonpdu.py b/confluent_server/confluent/plugins/hardwaremanagement/eatonpdu.py index da60b182..16be5b38 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/eatonpdu.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/eatonpdu.py @@ -158,7 +158,7 @@ def get_sensor_data(element, node, configmanager): sn = _sensors_by_node.get(node, None) for outlet in sn[0]: for sensename in sn[0][outlet]: - myname = '{0} {1}'.format(outlet, sensename) + myname = 'Outlet {0} {1}'.format(outlet, sensename) measurement = sn[0][outlet][sensename] if name == 'all' or simplify_name(myname) == name: readings.append({ @@ -259,11 +259,6 @@ class PDUClient(object): url = '/config/gateway?page={}&sessionId={}&_dc={}'.format(suburl, self.sessid, int(time.time())) return wc.grab_response(url) - def do_request1(self, suburl): - wc = self.wc - url = '/config/gateway?page={}&sessionId={}'.format(suburl, self.sessid) - return wc.grab_response(url) - def logout(self): self.do_request('cgi_logout') @@ -279,51 +274,29 @@ class PDUClient(object): return def get_sensor_data(self): - rsp = self.do_request1('cgi_overview') - + rsp = self.do_request('cgi_pdu_outlets') data = sanitize_json(rsp[0]) data = json.loads(data) - data1 = data['data'][4][0][8] data = data['data'][0] sdata = {} for outdata in data: - outsense = {} - outletname = outdata[3] + outletname = outdata[0][0] + outsense['Energy'] = { + 'value': float(outdata[11] / 1000), + 'units': 'kwh', + 'type': 'Energy' + } outsense['Power'] = { - 'value': outdata[5], - 'units': 'kW', + 'value': float(outdata[4]), + 'units': 'w', 'type': 'Power', } sdata[outletname] = outsense - for outdata in data1: - - outsense = {} - outletname = outdata[0] - if type(outdata[1]) == str : - splitter = outdata[1].split(" ") - - if len(splitter) == 1: - splitter.append('w') - outsense['Power'] = { - 'value': splitter[0], - 'units': splitter[1], - 'type': 'Power', - } - elif type(outdata[1]) == list: - if type(outdata[1][1]) == float: - outletname=outletname.strip('
Since') - - outsense['Energy'] = { - 'value': outdata[1][0] / 1000, - 'units': 'kWh', - 'type': 'Energy', - } - sdata[outletname] = outsense return sdata def set_outlet(self, outlet, state): - rsp = self.do_request('cgi_overview') + rsp = self.do_request('cgi_pdu_outlets') data = sanitize_json(rsp[0]) data = json.loads(data) data = data['data'][0] From bf004fb7b97e426217337848b21f39a62bd86aad Mon Sep 17 00:00:00 2001 From: Wera Grzeda Date: Tue, 23 Apr 2024 15:53:35 +0200 Subject: [PATCH 012/146] cleaned geist.py --- .../plugins/hardwaremanagement/geist.py | 277 ++++++++++-------- 1 file changed, 156 insertions(+), 121 deletions(-) diff --git a/confluent_server/confluent/plugins/hardwaremanagement/geist.py b/confluent_server/confluent/plugins/hardwaremanagement/geist.py index 06af8675..d3fce9e1 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/geist.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/geist.py @@ -19,25 +19,29 @@ import confluent.exceptions as exc import eventlet.green.time as time import eventlet import eventlet.greenpool as greenpool -import json + + def simplify_name(name): - return name.lower().replace(' ', '_').replace('/', '-').replace( - '_-_', '-') + return name.lower().replace(" ", "_").replace("/", "-").replace("_-_", "-") + pdupool = greenpool.GreenPool(128) + def data_by_type(indata): databytype = {} for keyname in indata: obj = indata[keyname] - objtype = obj.get('type', None) + objtype = obj.get("type", None) if not objtype: continue if objtype in databytype: - raise Exception("Multiple instances of type {} not yet supported".format(objtype)) + raise Exception( + "Multiple instances of type {} not yet supported".format(objtype) + ) databytype[objtype] = obj - obj['keyname'] = keyname + obj["keyname"] = keyname return databytype @@ -59,198 +63,229 @@ class GeistClient(object): def wc(self): if self._wc: return self._wc - targcfg = self.configmanager.get_node_attributes(self.node, - ['hardwaremanagement.manager'], - decrypt=True) + targcfg = self.configmanager.get_node_attributes( + self.node, ["hardwaremanagement.manager"], decrypt=True + ) targcfg = targcfg.get(self.node, {}) - target = targcfg.get( - 'hardwaremanagement.manager', {}).get('value', None) + target = targcfg.get("hardwaremanagement.manager", {}).get("value", None) if not target: target = self.node - target = target.split('/', 1)[0] + target = target.split("/", 1)[0] cv = util.TLSCertVerifier( - self.configmanager, self.node, - 'pubkeys.tls_hardwaremanager').verify_cert + self.configmanager, self.node, "pubkeys.tls_hardwaremanager" + ).verify_cert self._wc = wc.SecureHTTPConnection(target, port=443, verifycallback=cv) return self._wc def login(self, configmanager): - credcfg = configmanager.get_node_attributes(self.node, - ['secret.hardwaremanagementuser', - 'secret.hardwaremanagementpassword'], - decrypt=True) + credcfg = configmanager.get_node_attributes( + self.node, + ["secret.hardwaremanagementuser", "secret.hardwaremanagementpassword"], + decrypt=True, + ) credcfg = credcfg.get(self.node, {}) - username = credcfg.get( - 'secret.hardwaremanagementuser', {}).get('value', None) - passwd = credcfg.get( - 'secret.hardwaremanagementpassword', {}).get('value', None) + username = credcfg.get("secret.hardwaremanagementuser", {}).get("value", None) + passwd = credcfg.get("secret.hardwaremanagementpassword", {}).get("value", None) if not isinstance(username, str): - username = username.decode('utf8') + username = username.decode("utf8") if not isinstance(passwd, str): - passwd = passwd.decode('utf8') + passwd = passwd.decode("utf8") if not username or not passwd: - raise Exception('Missing username or password') + raise Exception("Missing username or password") self.username = username rsp = self.wc.grab_json_response( - '/api/auth/{0}'.format(username), - {'cmd': 'login', 'data': {'password': passwd}}) + "/api/auth/{0}".format(username), + {"cmd": "login", "data": {"password": passwd}}, + ) - token = rsp['data']['token'] + token = rsp["data"]["token"] return token def logout(self): if self._token: - self.wc.grab_json_response('/api/auth/{0}'.format(self.username), - {'cmd': 'logout', 'token': self.token}) + self.wc.grab_json_response( + "/api/auth/{0}".format(self.username), + {"cmd": "logout", "token": self.token}, + ) self._token = None def get_outlet(self, outlet): - rsp = self.wc.grab_json_response('/api/dev') - rsp = rsp['data'] + rsp = self.wc.grab_json_response("/api/dev") + rsp = rsp["data"] dbt = data_by_type(rsp) - if 't3hd' in dbt: - del dbt['t3hd'] + if "t3hd" in dbt: + del dbt["t3hd"] if len(dbt) != 1: - raise Exception('Multiple PDUs not supported per pdu') + raise Exception("Multiple PDUs not supported per pdu") pdutype = list(dbt)[0] - outlet = dbt[pdutype]['outlet'][ str(int(outlet)- 1) ] -# print(outlet) - state = outlet['state'].split('2')[-1] + outlet = dbt[pdutype]["outlet"][str(int(outlet) - 1)] + + state = outlet["state"].split("2")[-1] return state def set_outlet(self, outlet, state): - rsp = self.wc.grab_json_response('/api/dev') - dbt = data_by_type(rsp['data']) - if 't3hd' in dbt: - del dbt['t3hd'] + rsp = self.wc.grab_json_response("/api/dev") + dbt = data_by_type(rsp["data"]) + if "t3hd" in dbt: + del dbt["t3hd"] if len(dbt) != 1: self.logout() - raise Exception('Multiple PDUs per endpoint not supported') - pdu = dbt[list(dbt)[0]]['keyname'] - outlet = int(outlet) - 1 + raise Exception("Multiple PDUs per endpoint not supported") + pdu = dbt[list(dbt)[0]]["keyname"] + outlet = int(outlet) - 1 rsp = self.wc.grab_json_response( - '/api/dev/{0}/outlet/{1}'.format(pdu, outlet), - {'cmd': 'control', 'token': self.token, - 'data': {'action': state, 'delay': False}}) + "/api/dev/{0}/outlet/{1}".format(pdu, outlet), + { + "cmd": "control", + "token": self.token, + "data": {"action": state, "delay": False}, + }, + ) -def process_measurement(keyname, name, enttype, entname, measurement, readings, category): - if measurement['type'] == 'realPower': - if category not in ('all', 'power'): + +def process_measurement( + keyname, name, enttype, entname, measurement, readings, category +): + if measurement["type"] == "realPower": + if category not in ("all", "power"): return - readtype = 'Real Power' - elif measurement['type'] == 'apparentPower': - if category not in ('all', 'power'): + readtype = "Real Power" + elif measurement["type"] == "apparentPower": + if category not in ("all", "power"): return - readtype = 'Apparent Power' - elif measurement['type'] == 'energy': - if category not in ('all', 'energy'): + readtype = "Apparent Power" + elif measurement["type"] == "energy": + if category not in ("all", "energy"): return - readtype = 'Energy' - elif measurement['type'] == 'voltage': - if category not in ('all',): + readtype = "Energy" + elif measurement["type"] == "voltage": + if category not in ("all",): return - readtype = 'Voltage' - elif measurement['type'] == 'current': - if category not in ('all',): + readtype = "Voltage" + elif measurement["type"] == "current": + if category not in ("all",): return - readtype = 'Current' - elif measurement['type'] == 'temperature': - readtype = 'Temperature' - elif measurement['type'] == 'dewpoint': - readtype = 'Dewpoint' - elif measurement['type'] == 'humidity': - readtype = 'Humidity' + readtype = "Current" + elif measurement["type"] == "temperature": + readtype = "Temperature" + elif measurement["type"] == "dewpoint": + readtype = "Dewpoint" + elif measurement["type"] == "humidity": + readtype = "Humidity" else: return - myname = entname + ' ' + readtype - if name != 'all' and simplify_name(myname) != name: + myname = entname + " " + readtype + if name != "all" and simplify_name(myname) != name: return - readings.append({ - 'name': myname, - 'value': float(measurement['value']), - 'units': measurement['units'], - 'type': readtype.split()[-1] - }) - + readings.append( + { + "name": myname, + "value": float(measurement["value"]), + "units": measurement["units"], + "type": readtype.split()[-1], + } + ) + def process_measurements(name, category, measurements, enttype, readings): for measure in util.natural_sort(list(measurements)): - measurement = measurements[measure]['measurement'] - entname = measurements[measure]['name'] + measurement = measurements[measure]["measurement"] + entname = measurements[measure]["name"] for measureid in measurement: - process_measurement(measure, name, enttype, entname, measurement[measureid], readings, category) - + process_measurement( + measure, + name, + enttype, + entname, + measurement[measureid], + readings, + category, + ) + _sensors_by_node = {} + + def read_sensors(element, node, configmanager): category, name = element[-2:] justnames = False if len(element) == 3: # just get names category = name - name = 'all' + name = "all" justnames = True - if category in ('leds, fans', 'temperature'): + if category in ("leds, fans", "temperature"): return sn = _sensors_by_node.get(node, None) if not sn or sn[1] < time.time(): gc = GeistClient(node, configmanager) - adev = gc.wc.grab_json_response('/api/dev') + adev = gc.wc.grab_json_response("/api/dev") _sensors_by_node[node] = (adev, time.time() + 1) sn = _sensors_by_node.get(node, None) - dbt = data_by_type(sn[0]['data']) + dbt = data_by_type(sn[0]["data"]) readings = [] - for datatype in dbt: + for datatype in dbt: datum = dbt[datatype] - process_measurements(name, category, datum['entity'], 'entity', readings) + process_measurements(name, category, datum["entity"], "entity", readings) - if 'outlet' in datum: - process_measurements(name, category, datum['outlet'], 'outlet', readings) + if "outlet" in datum: + process_measurements(name, category, datum["outlet"], "outlet", readings) if justnames: for reading in readings: - yield msg.ChildCollection(simplify_name(reading['name'])) + yield msg.ChildCollection(simplify_name(reading["name"])) else: yield msg.SensorReadings(readings, name=node) + def get_outlet(element, node, configmanager): gc = GeistClient(node, configmanager) state = gc.get_outlet(element[-1]) return msg.PowerState(node=node, state=state) + def read_firmware(node, configmanager): gc = GeistClient(node, configmanager) - adev = gc.wc.grab_json_response('/api/sys') - myversion = adev['data']['version'] - yield msg.Firmware([{'PDU Firmware': {'version': myversion}}], node) + adev = gc.wc.grab_json_response("/api/sys") + myversion = adev["data"]["version"] + yield msg.Firmware([{"PDU Firmware": {"version": myversion}}], node) def read_inventory(element, node, configmanager): _inventory = {} inventory = {} gc = GeistClient(node, configmanager) - adev = gc.wc.grab_json_response('/api/sys') - basedata = adev['data'] - inventory['present'] = True - inventory['name'] = 'PDU' + adev = gc.wc.grab_json_response("/api/sys") + basedata = adev["data"] + inventory["present"] = True + inventory["name"] = "PDU" for elem in basedata.items(): - if elem[0] !='component' and elem[0] !='locale' and elem[0] !='state' and elem[0] !='contact' and elem[0] !='appVersion' and elem[0] !='build' and elem[0] !='version' and elem[0] !='apiVersion': + if ( + elem[0] != "component" + and elem[0] != "locale" + and elem[0] != "state" + and elem[0] != "contact" + and elem[0] != "appVersion" + and elem[0] != "build" + and elem[0] != "version" + and elem[0] != "apiVersion" + ): temp = elem[0] if elem[0] == "serialNumber": temp = "Serial" - elif elem[0] == "partNumber": + elif elem[0] == "partNumber": temp = "P/N" - elif elem[0] == "modelNumber": - temp= "Lenovo P/N and Serial" + elif elem[0] == "modelNumber": + temp = "Lenovo P/N and Serial" _inventory[temp] = elem[1] - elif elem[0] =='component': - tempname = '' - for component in basedata['component'].items(): + elif elem[0] == "component": + tempname = "" + for component in basedata["component"].items(): for item in component: if type(item) == str: @@ -258,28 +293,27 @@ def read_inventory(element, node, configmanager): else: for entry in item.items(): temp = entry[0] - if temp == 'sn': + if temp == "sn": temp = "Serial" - _inventory[tempname + ' ' + temp] = entry[1] - + _inventory[tempname + " " + temp] = entry[1] - inventory['information']= _inventory + inventory["information"] = _inventory + + yield msg.KeyValueData({"inventory": [inventory]}, node) - yield msg.KeyValueData({'inventory': [inventory]}, node) - def retrieve(nodes, element, configmanager, inputdata): - if 'outlets' in element: + if "outlets" in element: gp = greenpool.GreenPile(pdupool) for node in nodes: gp.spawn(get_outlet, element, node, configmanager) for res in gp: yield res - + return - elif element[0] == 'sensors': + elif element[0] == "sensors": gp = greenpool.GreenPile(pdupool) for node in nodes: gp.spawn(read_sensors, element, node, configmanager) @@ -287,7 +321,7 @@ def retrieve(nodes, element, configmanager, inputdata): for datum in rsp: yield datum return - elif '/'.join(element).startswith('inventory/firmware/all'): + elif "/".join(element).startswith("inventory/firmware/all"): gp = greenpool.GreenPile(pdupool) for node in nodes: gp.spawn(read_firmware, node, configmanager) @@ -295,7 +329,7 @@ def retrieve(nodes, element, configmanager, inputdata): for datum in rsp: yield datum - elif '/'.join(element).startswith('inventory/hardware/all'): + elif "/".join(element).startswith("inventory/hardware/all"): gp = greenpool.GreenPile(pdupool) for node in nodes: gp.spawn(read_inventory, element, node, configmanager) @@ -304,12 +338,13 @@ def retrieve(nodes, element, configmanager, inputdata): yield datum else: for node in nodes: - yield msg.ConfluentResourceUnavailable(node, 'Not implemented') + yield msg.ConfluentResourceUnavailable(node, "Not implemented") return - + + def update(nodes, element, configmanager, inputdata): - if 'outlets' not in element: - yield msg.ConfluentResourceUnavailable(node, 'Not implemented') + if "outlets" not in element: + yield msg.ConfluentResourceUnavailable(node, "Not implemented") return for node in nodes: gc = GeistClient(node, configmanager) From d779f015d6c35d1f459886f9ab9f9b63b808b374 Mon Sep 17 00:00:00 2001 From: Wera Grzeda Date: Tue, 23 Apr 2024 18:14:48 +0200 Subject: [PATCH 013/146] sed to use single quotes --- .../plugins/hardwaremanagement/geist.py | 222 +++++++++--------- 1 file changed, 111 insertions(+), 111 deletions(-) diff --git a/confluent_server/confluent/plugins/hardwaremanagement/geist.py b/confluent_server/confluent/plugins/hardwaremanagement/geist.py index d3fce9e1..3f086115 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/geist.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/geist.py @@ -1,13 +1,13 @@ # Copyright 2022 Lenovo # -# Licensed under the Apache License, Version 2.0 (the "License"); +# Licensed under the Apache License, Version 2.0 (the 'License'); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, +# distributed under the License is distributed on an 'AS IS' BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. @@ -23,7 +23,7 @@ import eventlet.greenpool as greenpool def simplify_name(name): - return name.lower().replace(" ", "_").replace("/", "-").replace("_-_", "-") + return name.lower().replace(' ', '_').replace('/', '-').replace('_-_', '-') pdupool = greenpool.GreenPool(128) @@ -33,15 +33,15 @@ def data_by_type(indata): databytype = {} for keyname in indata: obj = indata[keyname] - objtype = obj.get("type", None) + objtype = obj.get('type', None) if not objtype: continue if objtype in databytype: raise Exception( - "Multiple instances of type {} not yet supported".format(objtype) + 'Multiple instances of type {} not yet supported'.format(objtype) ) databytype[objtype] = obj - obj["keyname"] = keyname + obj['keyname'] = keyname return databytype @@ -64,15 +64,15 @@ class GeistClient(object): if self._wc: return self._wc targcfg = self.configmanager.get_node_attributes( - self.node, ["hardwaremanagement.manager"], decrypt=True + self.node, ['hardwaremanagement.manager'], decrypt=True ) targcfg = targcfg.get(self.node, {}) - target = targcfg.get("hardwaremanagement.manager", {}).get("value", None) + target = targcfg.get('hardwaremanagement.manager', {}).get('value', None) if not target: target = self.node - target = target.split("/", 1)[0] + target = target.split('/', 1)[0] cv = util.TLSCertVerifier( - self.configmanager, self.node, "pubkeys.tls_hardwaremanager" + self.configmanager, self.node, 'pubkeys.tls_hardwaremanager' ).verify_cert self._wc = wc.SecureHTTPConnection(target, port=443, verifycallback=cv) return self._wc @@ -80,68 +80,68 @@ class GeistClient(object): def login(self, configmanager): credcfg = configmanager.get_node_attributes( self.node, - ["secret.hardwaremanagementuser", "secret.hardwaremanagementpassword"], + ['secret.hardwaremanagementuser', 'secret.hardwaremanagementpassword'], decrypt=True, ) credcfg = credcfg.get(self.node, {}) - username = credcfg.get("secret.hardwaremanagementuser", {}).get("value", None) - passwd = credcfg.get("secret.hardwaremanagementpassword", {}).get("value", None) + username = credcfg.get('secret.hardwaremanagementuser', {}).get('value', None) + passwd = credcfg.get('secret.hardwaremanagementpassword', {}).get('value', None) if not isinstance(username, str): - username = username.decode("utf8") + username = username.decode('utf8') if not isinstance(passwd, str): - passwd = passwd.decode("utf8") + passwd = passwd.decode('utf8') if not username or not passwd: - raise Exception("Missing username or password") + raise Exception('Missing username or password') self.username = username rsp = self.wc.grab_json_response( - "/api/auth/{0}".format(username), - {"cmd": "login", "data": {"password": passwd}}, + '/api/auth/{0}'.format(username), + {'cmd': 'login', 'data': {'password': passwd}}, ) - token = rsp["data"]["token"] + token = rsp['data']['token'] return token def logout(self): if self._token: self.wc.grab_json_response( - "/api/auth/{0}".format(self.username), - {"cmd": "logout", "token": self.token}, + '/api/auth/{0}'.format(self.username), + {'cmd': 'logout', 'token': self.token}, ) self._token = None def get_outlet(self, outlet): - rsp = self.wc.grab_json_response("/api/dev") - rsp = rsp["data"] + rsp = self.wc.grab_json_response('/api/dev') + rsp = rsp['data'] dbt = data_by_type(rsp) - if "t3hd" in dbt: - del dbt["t3hd"] + if 't3hd' in dbt: + del dbt['t3hd'] if len(dbt) != 1: - raise Exception("Multiple PDUs not supported per pdu") + raise Exception('Multiple PDUs not supported per pdu') pdutype = list(dbt)[0] - outlet = dbt[pdutype]["outlet"][str(int(outlet) - 1)] + outlet = dbt[pdutype]['outlet'][str(int(outlet) - 1)] - state = outlet["state"].split("2")[-1] + state = outlet['state'].split('2')[-1] return state def set_outlet(self, outlet, state): - rsp = self.wc.grab_json_response("/api/dev") - dbt = data_by_type(rsp["data"]) - if "t3hd" in dbt: - del dbt["t3hd"] + rsp = self.wc.grab_json_response('/api/dev') + dbt = data_by_type(rsp['data']) + if 't3hd' in dbt: + del dbt['t3hd'] if len(dbt) != 1: self.logout() - raise Exception("Multiple PDUs per endpoint not supported") - pdu = dbt[list(dbt)[0]]["keyname"] + raise Exception('Multiple PDUs per endpoint not supported') + pdu = dbt[list(dbt)[0]]['keyname'] outlet = int(outlet) - 1 rsp = self.wc.grab_json_response( - "/api/dev/{0}/outlet/{1}".format(pdu, outlet), + '/api/dev/{0}/outlet/{1}'.format(pdu, outlet), { - "cmd": "control", - "token": self.token, - "data": {"action": state, "delay": False}, + 'cmd': 'control', + 'token': self.token, + 'data': {'action': state, 'delay': False}, }, ) @@ -149,51 +149,51 @@ class GeistClient(object): def process_measurement( keyname, name, enttype, entname, measurement, readings, category ): - if measurement["type"] == "realPower": - if category not in ("all", "power"): + if measurement['type'] == 'realPower': + if category not in ('all', 'power'): return - readtype = "Real Power" - elif measurement["type"] == "apparentPower": - if category not in ("all", "power"): + readtype = 'Real Power' + elif measurement['type'] == 'apparentPower': + if category not in ('all', 'power'): return - readtype = "Apparent Power" - elif measurement["type"] == "energy": - if category not in ("all", "energy"): + readtype = 'Apparent Power' + elif measurement['type'] == 'energy': + if category not in ('all', 'energy'): return - readtype = "Energy" - elif measurement["type"] == "voltage": - if category not in ("all",): + readtype = 'Energy' + elif measurement['type'] == 'voltage': + if category not in ('all',): return - readtype = "Voltage" - elif measurement["type"] == "current": - if category not in ("all",): + readtype = 'Voltage' + elif measurement['type'] == 'current': + if category not in ('all',): return - readtype = "Current" - elif measurement["type"] == "temperature": - readtype = "Temperature" - elif measurement["type"] == "dewpoint": - readtype = "Dewpoint" - elif measurement["type"] == "humidity": - readtype = "Humidity" + readtype = 'Current' + elif measurement['type'] == 'temperature': + readtype = 'Temperature' + elif measurement['type'] == 'dewpoint': + readtype = 'Dewpoint' + elif measurement['type'] == 'humidity': + readtype = 'Humidity' else: return - myname = entname + " " + readtype - if name != "all" and simplify_name(myname) != name: + myname = entname + ' ' + readtype + if name != 'all' and simplify_name(myname) != name: return readings.append( { - "name": myname, - "value": float(measurement["value"]), - "units": measurement["units"], - "type": readtype.split()[-1], + 'name': myname, + 'value': float(measurement['value']), + 'units': measurement['units'], + 'type': readtype.split()[-1], } ) def process_measurements(name, category, measurements, enttype, readings): for measure in util.natural_sort(list(measurements)): - measurement = measurements[measure]["measurement"] - entname = measurements[measure]["name"] + measurement = measurements[measure]['measurement'] + entname = measurements[measure]['name'] for measureid in measurement: process_measurement( measure, @@ -215,28 +215,28 @@ def read_sensors(element, node, configmanager): if len(element) == 3: # just get names category = name - name = "all" + name = 'all' justnames = True - if category in ("leds, fans", "temperature"): + if category in ('leds, fans', 'temperature'): return sn = _sensors_by_node.get(node, None) if not sn or sn[1] < time.time(): gc = GeistClient(node, configmanager) - adev = gc.wc.grab_json_response("/api/dev") + adev = gc.wc.grab_json_response('/api/dev') _sensors_by_node[node] = (adev, time.time() + 1) sn = _sensors_by_node.get(node, None) - dbt = data_by_type(sn[0]["data"]) + dbt = data_by_type(sn[0]['data']) readings = [] for datatype in dbt: datum = dbt[datatype] - process_measurements(name, category, datum["entity"], "entity", readings) + process_measurements(name, category, datum['entity'], 'entity', readings) - if "outlet" in datum: - process_measurements(name, category, datum["outlet"], "outlet", readings) + if 'outlet' in datum: + process_measurements(name, category, datum['outlet'], 'outlet', readings) if justnames: for reading in readings: - yield msg.ChildCollection(simplify_name(reading["name"])) + yield msg.ChildCollection(simplify_name(reading['name'])) else: yield msg.SensorReadings(readings, name=node) @@ -250,42 +250,42 @@ def get_outlet(element, node, configmanager): def read_firmware(node, configmanager): gc = GeistClient(node, configmanager) - adev = gc.wc.grab_json_response("/api/sys") - myversion = adev["data"]["version"] - yield msg.Firmware([{"PDU Firmware": {"version": myversion}}], node) + adev = gc.wc.grab_json_response('/api/sys') + myversion = adev['data']['version'] + yield msg.Firmware([{'PDU Firmware': {'version': myversion}}], node) def read_inventory(element, node, configmanager): _inventory = {} inventory = {} gc = GeistClient(node, configmanager) - adev = gc.wc.grab_json_response("/api/sys") - basedata = adev["data"] - inventory["present"] = True - inventory["name"] = "PDU" + adev = gc.wc.grab_json_response('/api/sys') + basedata = adev['data'] + inventory['present'] = True + inventory['name'] = 'PDU' for elem in basedata.items(): if ( - elem[0] != "component" - and elem[0] != "locale" - and elem[0] != "state" - and elem[0] != "contact" - and elem[0] != "appVersion" - and elem[0] != "build" - and elem[0] != "version" - and elem[0] != "apiVersion" + elem[0] != 'component' + and elem[0] != 'locale' + and elem[0] != 'state' + and elem[0] != 'contact' + and elem[0] != 'appVersion' + and elem[0] != 'build' + and elem[0] != 'version' + and elem[0] != 'apiVersion' ): temp = elem[0] - if elem[0] == "serialNumber": - temp = "Serial" - elif elem[0] == "partNumber": - temp = "P/N" - elif elem[0] == "modelNumber": - temp = "Lenovo P/N and Serial" + if elem[0] == 'serialNumber': + temp = 'Serial' + elif elem[0] == 'partNumber': + temp = 'P/N' + elif elem[0] == 'modelNumber': + temp = 'Lenovo P/N and Serial' _inventory[temp] = elem[1] - elif elem[0] == "component": - tempname = "" - for component in basedata["component"].items(): + elif elem[0] == 'component': + tempname = '' + for component in basedata['component'].items(): for item in component: if type(item) == str: @@ -293,18 +293,18 @@ def read_inventory(element, node, configmanager): else: for entry in item.items(): temp = entry[0] - if temp == "sn": - temp = "Serial" - _inventory[tempname + " " + temp] = entry[1] + if temp == 'sn': + temp = 'Serial' + _inventory[tempname + ' ' + temp] = entry[1] - inventory["information"] = _inventory + inventory['information'] = _inventory - yield msg.KeyValueData({"inventory": [inventory]}, node) + yield msg.KeyValueData({'inventory': [inventory]}, node) def retrieve(nodes, element, configmanager, inputdata): - if "outlets" in element: + if 'outlets' in element: gp = greenpool.GreenPile(pdupool) for node in nodes: @@ -313,7 +313,7 @@ def retrieve(nodes, element, configmanager, inputdata): yield res return - elif element[0] == "sensors": + elif element[0] == 'sensors': gp = greenpool.GreenPile(pdupool) for node in nodes: gp.spawn(read_sensors, element, node, configmanager) @@ -321,7 +321,7 @@ def retrieve(nodes, element, configmanager, inputdata): for datum in rsp: yield datum return - elif "/".join(element).startswith("inventory/firmware/all"): + elif '/'.join(element).startswith('inventory/firmware/all'): gp = greenpool.GreenPile(pdupool) for node in nodes: gp.spawn(read_firmware, node, configmanager) @@ -329,7 +329,7 @@ def retrieve(nodes, element, configmanager, inputdata): for datum in rsp: yield datum - elif "/".join(element).startswith("inventory/hardware/all"): + elif '/'.join(element).startswith('inventory/hardware/all'): gp = greenpool.GreenPile(pdupool) for node in nodes: gp.spawn(read_inventory, element, node, configmanager) @@ -338,13 +338,13 @@ def retrieve(nodes, element, configmanager, inputdata): yield datum else: for node in nodes: - yield msg.ConfluentResourceUnavailable(node, "Not implemented") + yield msg.ConfluentResourceUnavailable(node, 'Not implemented') return def update(nodes, element, configmanager, inputdata): - if "outlets" not in element: - yield msg.ConfluentResourceUnavailable(node, "Not implemented") + if 'outlets' not in element: + yield msg.ConfluentResourceUnavailable(node, 'Not implemented') return for node in nodes: gc = GeistClient(node, configmanager) From d231326dfbe0cdd70b7d258f2172ccf5d98bfa17 Mon Sep 17 00:00:00 2001 From: Simon Thompson Date: Fri, 21 Jun 2024 18:34:47 +0200 Subject: [PATCH 014/146] add class to run cmd by ssh --- .../confluent/plugins/shell/ssh.py | 112 +++++++++++++++++- 1 file changed, 110 insertions(+), 2 deletions(-) diff --git a/confluent_server/confluent/plugins/shell/ssh.py b/confluent_server/confluent/plugins/shell/ssh.py index 2a6b65ec..f802f842 100644 --- a/confluent_server/confluent/plugins/shell/ssh.py +++ b/confluent_server/confluent/plugins/shell/ssh.py @@ -43,7 +43,6 @@ if cryptography and cryptography.__version__.split('.') < ['1', '5']: paramiko.transport.Transport._preferred_keys) - class HostKeyHandler(paramiko.client.MissingHostKeyPolicy): def __init__(self, configmanager, node): @@ -112,7 +111,7 @@ class SshShell(conapi.Console): # that would rather not use the nodename as anything but an opaque # identifier self.datacallback = callback - if self.username is not b'': + if self.username != b'': self.logon() else: self.inputmode = 0 @@ -259,6 +258,115 @@ class SshShell(conapi.Console): self.ssh.close() self.datacallback = None + def create(nodes, element, configmanager, inputdata): if len(nodes) == 1: return SshShell(nodes[0], configmanager) + + +class SshConn(): + + def __init__(self, node, config, username=b'', password=b''): + self.node = node + self.ssh = None + self.datacallback = None + self.nodeconfig = config + self.username = username + self.password = password + self.connected = False + self.inputmode = 0 # 0 = username, 1 = password... + + def __del__(self): + if self.connected: + self.close() + + def do_logon(self): + self.ssh = paramiko.SSHClient() + self.ssh.set_missing_host_key_policy( + HostKeyHandler(self.nodeconfig, self.node)) + log.log({'info': f"Connecting to {self.node} by ssh"}) + try: + if self.password: + self.ssh.connect(self.node, username=self.username, + password=self.password, allow_agent=False, + look_for_keys=False) + else: + self.ssh.connect(self.node, username=self.username) + except paramiko.AuthenticationException as e: + self.ssh.close() + self.inputmode = 0 + self.username = b'' + self.password = b'' + log.log({'warn': f"Error connecting to {self.node}: {str(e)}"}) + return + except paramiko.ssh_exception.NoValidConnectionsError as e: + self.ssh.close() + self.inputmode = 0 + self.username = b'' + self.password = b'' + log.log({'warn': f"Error connecting to {self.node}: {str(e)}"}) + return + except cexc.PubkeyInvalid as pi: + self.ssh.close() + self.keyaction = b'' + self.candidatefprint = pi.fingerprint + log.log({'warn': pi.message}) + self.keyattrname = pi.attrname + log.log({'info': f"New fingerprint: {pi.fingerprint}"}) + self.inputmode = -1 + return + except paramiko.SSHException as pi: + self.ssh.close() + self.inputmode = -2 + warn = str(pi) + if warnhostkey: + warn += ' (Older cryptography package on this host only ' \ + 'works with ed25519, check ssh startup on target ' \ + 'and permissions on /etc/ssh/*key)\r\n' + log.log({'warn': warn}) + return + except Exception as e: + self.ssh.close() + self.ssh.close() + self.inputmode = 0 + self.username = b'' + self.password = b'' + log.log({'warn': f"Error connecting to {self.node}: {str(e)}"}) + return + self.inputmode = 2 + self.connected = True + log.log({'info': f"Connected by ssh to {self.node}"}) + + def exec_command(self, cmd, cmdargs): + safecmd = cmd.translate(str.maketrans({"[": r"\]", + "]": r"\]", + "?": r"\?", + "!": r"\!", + "\\": r"\\", + "^": r"\^", + "$": r"\$", + " ": r"\ ", + "*": r"\*"})) + cmds = [safecmd] + for arg in cmdargs: + arg = arg.translate(str.maketrans({"[": r"\]", + "]": r"\]", + "?": r"\?", + "!": r"\!", + "\\": r"\\", + "^": r"\^", + "$": r"\$", + " ": r"\ ", + "*": r"\*"})) + arg = "%s" % (str(arg).replace(r"'", r"'\''"),) + cmds.append(arg) + + runcmd = " ".join(cmds) + stdin, stdout, stderr = self.ssh.exec_command(runcmd) + rcode = stdout.channel.recv_exit_status() + return stdout.readlines(), stderr.readlines() + + def close(self): + if self.ssh is not None: + self.ssh.close() + log.log({'info': f"Disconnected from {self.node}"}) From 166e4599b97fa47e9a6546b084c34f05b943674f Mon Sep 17 00:00:00 2001 From: Simon Thompson Date: Fri, 21 Jun 2024 18:35:10 +0200 Subject: [PATCH 015/146] add enos management plugin --- confluent_server/confluent/core.py | 2 +- .../plugins/hardwaremanagement/enos.py | 347 ++++++++++++++++++ 2 files changed, 348 insertions(+), 1 deletion(-) create mode 100644 confluent_server/confluent/plugins/hardwaremanagement/enos.py diff --git a/confluent_server/confluent/core.py b/confluent_server/confluent/core.py index a8a4412b..ce792fcb 100644 --- a/confluent_server/confluent/core.py +++ b/confluent_server/confluent/core.py @@ -73,7 +73,7 @@ import sys import yaml pluginmap = {} -dispatch_plugins = (b'ipmi', u'ipmi', b'redfish', u'redfish', b'tsmsol', u'tsmsol', b'geist', u'geist', b'deltapdu', u'deltapdu', b'eatonpdu', u'eatonpdu', b'affluent', u'affluent', b'cnos', u'cnos') +dispatch_plugins = (b'ipmi', u'ipmi', b'redfish', u'redfish', b'tsmsol', u'tsmsol', b'geist', u'geist', b'deltapdu', u'deltapdu', b'eatonpdu', u'eatonpdu', b'affluent', u'affluent', b'cnos', u'cnos', b'enos', u'enos') PluginCollection = plugin.PluginCollection diff --git a/confluent_server/confluent/plugins/hardwaremanagement/enos.py b/confluent_server/confluent/plugins/hardwaremanagement/enos.py new file mode 100644 index 00000000..f568fae2 --- /dev/null +++ b/confluent_server/confluent/plugins/hardwaremanagement/enos.py @@ -0,0 +1,347 @@ + +# Copyright 2019 Lenovo +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +#Noncritical: +# - One or more temperature sensors is in the warning range; +#Critical: +# - One or more temperature sensors is in the failure range; +# - One or more fans are running < 100 RPM; +# - One power supply is off. + +import re +import eventlet +import eventlet.queue as queue +import confluent.exceptions as exc +webclient = eventlet.import_patched("pyghmi.util.webclient") +import confluent.messages as msg +import confluent.util as util +import confluent.plugins.shell.ssh as ssh + + +class SwitchSensor(object): + def __init__(self, name, states=None, units=None, value=None, health=None): + self.name = name + self.value = value + self.states = states + self.health = health + self.units = units + + +def _run_method(method, workers, results, configmanager, nodes, element): + creds = configmanager.get_node_attributes( + nodes, ["switchuser", "switchpass", "secret.hardwaremanagementpassword", + "secret.hardwaremanagementuser"], decrypt=True) + for node in nodes: + workers.add(eventlet.spawn(method, configmanager, creds, + node, results, element)) + + +def enos_login(node, configmanager, creds): + try: + ukey = "switchuser" + upass = "switchpass" + if ukey not in creds and "secret.hardwaremanagementuser" in creds[node]: + ukey = "secret.hardwaremanagementuser" + upass = "secret.hardwaremanagementpassword" + + if ukey not in creds[node]: + raise exc.TargetEndpointBadCredentials("Unable to authenticate - switchuser or secret.hardwaremanagementuser not set") + user = creds[node][ukey]["value"] + if upass not in creds[node]: + passwd = None + else: + passwd = creds[node][upass]["value"] + nssh = ssh.SshConn(node=node, config=configmanager, username=user, password=passwd) + nssh.do_logon() + return nssh + except Exception as e: + raise exc.TargetEndpointBadCredentials(f"Unable to authenticate {e}") + + +def enos_version(ssh): + sshStdout, sshStderr = ssh.exec_command(cmd="show", cmdargs=["version"]) + return sshStdout + + +def update(nodes, element, configmanager, inputdata): + for node in nodes: + yield msg.ConfluentNodeError(node, "Not Implemented") + + +def delete(nodes, element, configmanager, inputdata): + for node in nodes: + yield msg.ConfluentNodeError(node, "Not Implemented") + + +def create(nodes, element, configmanager, inputdata): + for node in nodes: + yield msg.ConfluentNodeError(node, "Not Implemented") + + +def retrieve(nodes, element, configmanager, inputdata): + results = queue.LightQueue() + workers = set([]) + if element == ["power", "state"]: + for node in nodes: + yield msg.PowerState(node=node, state="on") + return + elif element == ["health", "hardware"]: + _run_method(retrieve_health, workers, results, configmanager, nodes, element) + elif element[:3] == ["inventory", "hardware", "all"]: + _run_method(retrieve_inventory, workers, results, configmanager, nodes, element) + elif element[:3] == ["inventory", "firmware", "all"]: + _run_method(retrieve_firmware, workers, results, configmanager, nodes, element) + elif element[:3] == ["sensors", "hardware", "all"]: + _run_method(retrieve_sensors, workers, results, configmanager, nodes, element) + else: + for node in nodes: + yield msg.ConfluentNodeError(node, f"Not Implemented: {element}") + return + currtimeout = 10 + while workers: + try: + datum = results.get(10) + while datum: + if datum: + yield datum + datum = results.get_nowait() + except queue.Empty: + pass + eventlet.sleep(0.001) + for t in list(workers): + if t.dead: + workers.discard(t) + try: + while True: + datum = results.get_nowait() + if datum: + yield datum + except queue.Empty: + pass + + +def retrieve_inventory(configmanager, creds, node, results, element): + if len(element) == 3: + results.put(msg.ChildCollection("all")) + results.put(msg.ChildCollection("system")) + return + + switch = gather_data(configmanager, creds, node) + invinfo = switch["inventory"] + + for fan, data in switch["fans"].items(): + invinfo["inventory"][0]["information"][f"Fan #{fan}"] = data["state"] + + for psu, data in switch["psus"].items(): + invinfo["inventory"][0]["information"][f"PSU #{psu}"] = data["state"] + + results.put(msg.KeyValueData(invinfo, node)) + + +def gather_data(configmanager, creds, node): + nssh = enos_login(node=node, configmanager=configmanager, creds=creds) + switch_lines = enos_version(ssh=nssh) + switch_data = {} + sysinfo = {"Product name": {"regex": ".*RackSwitch (\w+)"}, + "Serial Number": {"regex": "ESN\s*\w*\s*: ([\w-]+)"}, + "Board Serial Number": {"regex": "Switch Serial No: (\w+)"}, + "Model": {"regex": "MTM\s*\w*\s*: ([\w-]+)"}, + "FRU Number": {"regex": "Hardware Part\s*\w*\s*: (\w+)"}, + "Airflow": {"regex": "System Fan Airflow\s*\w*\s*: ([\w-]+)"}, + } + + invinfo = { + "inventory": [{ + "name": "System", + "present": True, + "information": { + "Manufacturer": "Lenovo", + } + }] + } + + switch_data["sensors"] = [] + + switch_data["fans"] = gather_fans(switch_lines) + for fan, data in switch_data["fans"].items(): + if "rpm" in data: + health = "ok" + if int(data["rpm"]) < 100: + health = "critical" + switch_data["sensors"].append(SwitchSensor(name=f"Fan {fan}", value=data['rpm'], + units="RPM", health=health)) + + switch_data["psus"] = gather_psus(switch_lines) + + # Hunt for the temp limits + phylimit = {"warn": None, "shut": None} + templimit = {"warn": None, "shut": None} + for line in switch_lines: + match = re.match(r"([\w\s]+)Warning[\w\s]+\s(\d+)[\sA-Za-z\/]+\s(\d+)[\s\w\/]+\s(\d*)", line) + if match: + if "System" in match.group(1): + templimit["warn"] = int(match.group(2)) + templimit["shut"] = int(match.group(3)) + elif "PHYs" in match.group(1): + phylimit["warn"] = int(match.group(2)) + phylimit["shut"] = int(match.group(3)) + if not phylimit["warn"]: + phylimit = templimit + + for line in switch_lines: + # match the inventory data + for key in sysinfo.keys(): + match = re.match(re.compile(sysinfo[key]["regex"]), line) + if match: + invinfo["inventory"][0]["information"][key] = match.group(1).strip() + + # match temp sensors logging where failed + match = re.match(r"Temperature\s+([\d\s\w]+)\s*:\s*(\d+)+\s+([CF])+", line) + if match: + health = "ok" + temp = int(match.group(2)) + name = f"{match.group(1).strip()} Temp" + if "Phy" in name: + if temp > phylimit["warn"]: + health = "warning" + if temp > phylimit["shut"]: + health = "critical" + else: + if temp > templimit["warn"]: + health = "warning" + if temp > templimit["shut"]: + health = "critical" + switch_data["sensors"].append(SwitchSensor(name=name, + value=temp, units=f"°{match.group(3)}", health=health)) + match = re.match(r"\s*(\w+) Faults\s*:\s+(.+)", line) + if match and match.group(2) not in ["()", "None"]: + switch_data["sensors"].append(SwitchSensor(name=f"{match.group(1)} Fault", + value=match.group(2).strip(), units="", health="critical")) + + switch_data["inventory"] = invinfo + + sysfw = {"Software Version": "Unknown", "Boot kernel": "Unknown"} + for line in switch_lines: + for key in sysfw.keys(): + regex = f"{key}\s*\w*\s* ([0-9.]+)" + match = re.match(re.compile(regex), line) + if match: + sysfw[key] = match.group(1) + switch_data["firmware"] = sysfw + + return switch_data + + +def gather_psus(data): + psus = {} + for line in data: + # some switches are: + # Power Supply 1: Back-To-Front + # others are: + # Internal Power Supply: On + if "Power Supply" in line: + match = re.match(re.compile(f"Power Supply (\d)+.*"), line) + if match: + psu = match.group(1) + if psu not in psus: + psus[psu] = {} + m = re.match(r".+\s+(\w+\-\w+\-\w+)\s*\[*.*$", line) + if m: + psus[psu]["airflow"] = m.group(1) + psus[psu]["state"] = "Present" + else: + psus[psu]["state"] = "Not installed" + else: + for psu in range(1, 10): + if "Power Supply" in line and psu not in psus: + if psu not in psus: + psus[psu] = {} + if "Not Installed" in line: + psus[psu]["state"] = "Not installed" + break + else: + psus[psu]["state"] = "Present" + break + return psus + + +def gather_fans(data): + fans = {} + for line in data: + # look for presence of fans + if "Fan" in line: + match = re.match(re.compile(f"Fan (\d)+.*"), line) + if match: + fan = match.group(1) + if match: + if fan not in fans: + fans[fan] = {} + if "rpm" in line or "RPM" in line: + if "Module" in line: + m = re.search(r"Module\s+(\d)+:", line) + if m: + fans[fan]["Module"] = m.group(1) + fans[fan]["state"] = "Present" + m = re.search(r"(\d+)\s*:\s+(RPM=)*(\d+)(rpm)*", line) + if m: + fans[fan]["rpm"] = m.group(3) + + m = re.search(r"\s+(PWM=)*(\d+)(%|pwm)+", line) + if m: + fans[fan]["pwm"] = m.group(2) + + m = re.search(r"(.+)\s+(\w+\-\w+\-\w+)$", line) + if m: + fans[fan]["airflow"] = m.group(1) + else: + fans[fan]["state"] = "Not installed" + return fans + + +def retrieve_firmware(configmanager, creds, node, results, element): + if len(element) == 3: + results.put(msg.ChildCollection("all")) + return + sysinfo = gather_data(configmanager, creds, node)["firmware"] + items = [{ + "Software": {"version": sysinfo["Software Version"]}, + }, + { + "Boot kernel": {"version": sysinfo["Boot kernel"]}, + }] + results.put(msg.Firmware(items, node)) + + +def retrieve_health(configmanager, creds, node, results, element): + switch = gather_data(configmanager, creds, node) + badreadings = [] + summary = "ok" + sensors = gather_data(configmanager, creds, node)["sensors"] + + for sensor in sensors: + if sensor.health not in ["ok"]: + if sensor.health in ["critical"]: + summary = "critical" + elif summary in ["ok"] and sensor.health in ["warning"]: + summary = "warning" + badreadings.append(sensor) + results.put(msg.HealthSummary(summary, name=node)) + results.put(msg.SensorReadings(badreadings, name=node)) + + +def retrieve_sensors(configmanager, creds, node, results, element): + sensors = gather_data(configmanager, creds, node)["sensors"] + results.put(msg.SensorReadings(sensors, node)) From f2d9c3868ba6ad3287cccd5c9b433d675a9a98f3 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 24 Jun 2024 15:56:26 -0400 Subject: [PATCH 016/146] Draft work on MegaRAC out of band discovery --- .../confluent/discovery/protocols/ssdp.py | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/confluent_server/confluent/discovery/protocols/ssdp.py b/confluent_server/confluent/discovery/protocols/ssdp.py index 3c1edc74..b5847965 100644 --- a/confluent_server/confluent/discovery/protocols/ssdp.py +++ b/confluent_server/confluent/discovery/protocols/ssdp.py @@ -86,6 +86,7 @@ def _process_snoop(peer, rsp, mac, known_peers, newmacs, peerbymacaddress, byeha 'hwaddr': mac, 'addresses': [peer], } + targurl = None for headline in rsp[1:]: if not headline: continue @@ -105,13 +106,18 @@ def _process_snoop(peer, rsp, mac, known_peers, newmacs, peerbymacaddress, byeha if not value.endswith('/redfish/v1/'): return elif header == 'LOCATION': - if not value.endswith('/DeviceDescription.json'): + if '/eth' in value and value.endswith('.xml'): + targurl = '/redfish/v1/' + continue # MegaRAC redfish + elif value.endswith('/DeviceDescription.json'): + targurl = '/DeviceDescription.json' + else: return - if handler: - eventlet.spawn_n(check_fish_handler, handler, peerdata, known_peers, newmacs, peerbymacaddress, machandlers, mac, peer) + if handler and targurl: + eventlet.spawn_n(check_fish_handler, handler, peerdata, known_peers, newmacs, peerbymacaddress, machandlers, mac, peer, targurl) -def check_fish_handler(handler, peerdata, known_peers, newmacs, peerbymacaddress, machandlers, mac, peer): - retdata = check_fish(('/DeviceDescription.json', peerdata)) +def check_fish_handler(handler, peerdata, known_peers, newmacs, peerbymacaddress, machandlers, mac, peer, targurl): + retdata = check_fish((targurl, peerdata)) if retdata: known_peers.add(peer) newmacs.add(mac) @@ -411,6 +417,10 @@ def _find_service(service, target): continue if '/DeviceDescription.json' in peerdata[nid]['urls']: pooltargs.append(('/DeviceDescription.json', peerdata[nid])) + else: + for targurl in peerdata[nid]['urls']: + if '/eth' in targurl and targurl.endswith('.xml'): + pooltargs.append(('/redfish/v1/', peerdata[nid])) # For now, don't interrogate generic redfish bmcs # This is due to a need to deduplicate from some supported SLP # targets (IMM, TSM, others) From 762a8ee73f3820f559124a6c237a05d30cdc8288 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 25 Jun 2024 16:25:19 -0400 Subject: [PATCH 017/146] Correct proxyDHCP buffer use It was possible for proxyDHCP to look past the network designated end of packet. Fix this by consistently using the memoryview that was trimmed to size. --- confluent_server/confluent/discovery/protocols/pxe.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/confluent_server/confluent/discovery/protocols/pxe.py b/confluent_server/confluent/discovery/protocols/pxe.py index 4a39654f..133d8abd 100644 --- a/confluent_server/confluent/discovery/protocols/pxe.py +++ b/confluent_server/confluent/discovery/protocols/pxe.py @@ -315,9 +315,9 @@ def proxydhcp(handler, nodeguess): optidx = rqv.tobytes().index(b'\x63\x82\x53\x63') + 4 except ValueError: continue - hwlen = rq[2] - opts, disco = opts_to_dict(rq, optidx, 3) - disco['hwaddr'] = ':'.join(['{0:02x}'.format(x) for x in rq[28:28+hwlen]]) + hwlen = rqv[2] + opts, disco = opts_to_dict(rqv, optidx, 3) + disco['hwaddr'] = ':'.join(['{0:02x}'.format(x) for x in rqv[28:28+hwlen]]) node = None if disco.get('hwaddr', None) in macmap: node = macmap[disco['hwaddr']] From 07005d83ca09784b47903fb44f34d02aca48ec6e Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 27 Jun 2024 11:25:33 -0400 Subject: [PATCH 018/146] Add MegaRAC discovery support for recent MegaRAC Create a generic redfish discovery and a MegaRAC specific variant. This should open the door for more generic common base redfish discovery for vaguely compatible implementations. For now, MegaRAC only overrides the default username and password (which is undefined in the redfish spec). Also, have SSDP recognize the variant, and tolerate odd nonsense like SSDP replies coming from all manner of odd port numbers (no way to make a sane firewall rule to capture that odd behavior, but at application level we have a chance). --- confluent_server/confluent/discovery/core.py | 9 +- .../confluent/discovery/handlers/megarac.py | 51 ++++ .../discovery/handlers/redfishbmc.py | 269 ++++++++++++++++++ .../confluent/discovery/protocols/ssdp.py | 52 +++- 4 files changed, 366 insertions(+), 15 deletions(-) create mode 100644 confluent_server/confluent/discovery/handlers/megarac.py create mode 100644 confluent_server/confluent/discovery/handlers/redfishbmc.py diff --git a/confluent_server/confluent/discovery/core.py b/confluent_server/confluent/discovery/core.py index dfb50b9f..7b94154a 100644 --- a/confluent_server/confluent/discovery/core.py +++ b/confluent_server/confluent/discovery/core.py @@ -74,6 +74,7 @@ import confluent.discovery.handlers.tsm as tsm import confluent.discovery.handlers.pxe as pxeh import confluent.discovery.handlers.smm as smm import confluent.discovery.handlers.xcc as xcc +import confluent.discovery.handlers.megarac as megarac import confluent.exceptions as exc import confluent.log as log import confluent.messages as msg @@ -113,6 +114,7 @@ nodehandlers = { 'service:lenovo-smm': smm, 'service:lenovo-smm2': smm, 'lenovo-xcc': xcc, + 'megarac-bmc': megarac, 'service:management-hardware.IBM:integrated-management-module2': imm, 'pxe-client': pxeh, 'onie-switch': None, @@ -132,6 +134,7 @@ servicenames = { 'service:lenovo-smm2': 'lenovo-smm2', 'affluent-switch': 'affluent-switch', 'lenovo-xcc': 'lenovo-xcc', + 'megarac-bmc': 'megarac-bmc', #'openbmc': 'openbmc', 'service:management-hardware.IBM:integrated-management-module2': 'lenovo-imm2', 'service:io-device.Lenovo:management-module': 'lenovo-switch', @@ -147,6 +150,7 @@ servicebyname = { 'lenovo-smm2': 'service:lenovo-smm2', 'affluent-switch': 'affluent-switch', 'lenovo-xcc': 'lenovo-xcc', + 'megarac-bmc': 'megarac-bmc', 'lenovo-imm2': 'service:management-hardware.IBM:integrated-management-module2', 'lenovo-switch': 'service:io-device.Lenovo:management-module', 'thinkagile-storage': 'service:thinkagile-storagebmc', @@ -453,7 +457,7 @@ def iterate_addrs(addrs, countonly=False): yield 1 return yield addrs - + def _parameterize_path(pathcomponents): listrequested = False childcoll = True @@ -542,7 +546,7 @@ def handle_api_request(configmanager, inputdata, operation, pathcomponents): if len(pathcomponents) > 2: raise Exception('TODO') currsubs = get_subscriptions() - return [msg.ChildCollection(x) for x in currsubs] + return [msg.ChildCollection(x) for x in currsubs] elif operation == 'retrieve': return handle_read_api_request(pathcomponents) elif (operation in ('update', 'create') and @@ -1703,3 +1707,4 @@ if __name__ == '__main__': start_detection() while True: eventlet.sleep(30) + diff --git a/confluent_server/confluent/discovery/handlers/megarac.py b/confluent_server/confluent/discovery/handlers/megarac.py new file mode 100644 index 00000000..d7d8786a --- /dev/null +++ b/confluent_server/confluent/discovery/handlers/megarac.py @@ -0,0 +1,51 @@ +# Copyright 2024 Lenovo +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import confluent.discovery.handlers.redfishbmc as redfishbmc +import eventlet.support.greendns + + +getaddrinfo = eventlet.support.greendns.getaddrinfo + + +class NodeHandler(redfishbmc.NodeHandler): + + def get_firmware_default_account_info(self): + return ('admin', 'admin') + + +def remote_nodecfg(nodename, cfm): + cfg = cfm.get_node_attributes( + nodename, 'hardwaremanagement.manager') + ipaddr = cfg.get(nodename, {}).get('hardwaremanagement.manager', {}).get( + 'value', None) + ipaddr = ipaddr.split('/', 1)[0] + ipaddr = getaddrinfo(ipaddr, 0)[0][-1] + if not ipaddr: + raise Exception('Cannot remote configure a system without known ' + 'address') + info = {'addresses': [ipaddr]} + nh = NodeHandler(info, cfm) + nh.config(nodename) + + +if __name__ == '__main__': + import confluent.config.configmanager as cfm + c = cfm.ConfigManager(None) + import sys + info = {'addresses': [[sys.argv[1]]]} + print(repr(info)) + testr = NodeHandler(info, c) + testr.config(sys.argv[2]) + diff --git a/confluent_server/confluent/discovery/handlers/redfishbmc.py b/confluent_server/confluent/discovery/handlers/redfishbmc.py new file mode 100644 index 00000000..eed401de --- /dev/null +++ b/confluent_server/confluent/discovery/handlers/redfishbmc.py @@ -0,0 +1,269 @@ +# Copyright 2024 Lenovo +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import confluent.discovery.handlers.generic as generic +import confluent.exceptions as exc +import confluent.netutil as netutil +import confluent.util as util +import eventlet +import eventlet.support.greendns +import json +try: + from urllib import urlencode +except ImportError: + from urllib.parse import urlencode + +getaddrinfo = eventlet.support.greendns.getaddrinfo + +webclient = eventlet.import_patched('pyghmi.util.webclient') + +def get_host_interface_urls(wc, mginfo): + returls = [] + hifurl = mginfo.get('HostInterfaces', {}).get('@odata.id', None) + if not hifurl: + return None + hifinfo = wc.grab_json_response(hifurl) + hifurls = hifinfo.get('Members', []) + for hifurl in hifurls: + hifurl = hifurl['@odata.id'] + hifinfo = wc.grab_json_response(hifurl) + acturl = hifinfo.get('ManagerEthernetInterface', {}).get('@odata.id', None) + if acturl: + returls.append(acturl) + return returls + + +class NodeHandler(generic.NodeHandler): + devname = 'BMC' + + def __init__(self, info, configmanager): + self.trieddefault = None + self.targuser = None + self.curruser = None + self.currpass = None + self.targpass = None + self.nodename = None + self.csrftok = None + self.channel = None + self.atdefault = True + super(NodeHandler, self).__init__(info, configmanager) + + def get_firmware_default_account_info(self): + raise Exception('This must be subclassed') + + def scan(self): + c = webclient.SecureHTTPConnection(self.ipaddr, 443, verifycallback=self.validate_cert) + i = c.grab_json_response('/redfish/v1/') + uuid = i.get('UUID', None) + if uuid: + self.info['uuid'] = uuid.lower() + + def validate_cert(self, certificate): + # broadly speaking, merely checks consistency moment to moment, + # but if https_cert gets stricter, this check means something + fprint = util.get_fingerprint(self.https_cert) + return util.cert_matches(fprint, certificate) + + def _get_wc(self): + defuser, defpass = self.get_firmware_default_account_info() + wc = webclient.SecureHTTPConnection(self.ipaddr, 443, verifycallback=self.validate_cert) + wc.set_basic_credentials(defuser, defpass) + wc.set_header('Content-Type', 'application/json') + authmode = 0 + if not self.trieddefault: + rsp, status = wc.grab_json_response_with_status('/redfish/v1/Managers') + if status == 403: + self.trieddefault = True + chgurl = None + rsp = json.loads(rsp) + currerr = rsp.get('error', {}) + ecode = currerr.get('code', None) + if ecode.endswith('PasswordChangeRequired'): + for einfo in currerr.get('@Message.ExtendedInfo', []): + if einfo.get('MessageId', None).endswith('PasswordChangeRequired'): + for msgarg in einfo.get('MessageArgs'): + chgurl = msgarg + break + if chgurl: + if self.targpass == defpass: + raise Exception("Must specify a non-default password to onboard this BMC") + wc.set_header('If-Match', '*') + cpr = wc.grab_json_response_with_status(chgurl, {'Password': self.targpass}, method='PATCH') + if cpr[1] >= 200 and cpr[1] < 300: + self.curruser = defuser + self.currpass = self.targpass + wc.set_basic_credentials(self.curruser, self.currpass) + _, status = wc.grab_json_response_with_status('/redfish/v1/Managers') + tries = 10 + while status >= 300 and tries: + eventlet.sleep(1) + _, status = wc.grab_json_response_with_status('/redfish/v1/Managers') + return wc + + if status > 400: + self.trieddefault = True + if status == 401: + wc.set_basic_credentials(self.DEFAULT_USER, self.targpass) + rsp, status = wc.grab_json_response_with_status('/redfish/v1/Managers') + if status == 200: # Default user still, but targpass + self.currpass = self.targpass + self.curruser = defuser + return wc + elif self.targuser != defuser: + wc.set_basic_credentials(self.targuser, self.targpass) + rsp, status = wc.grab_json_response_with_status('/redfish/v1/Managers') + if status != 200: + raise Exception("Target BMC does not recognize firmware default credentials nor the confluent stored credential") + else: + self.curruser = defuser + self.currpass = defpass + return wc + if self.curruser: + wc.set_basic_credentials(self.curruser, self.currpass) + rsp, status = wc.grab_json_response_with_status('/redfish/v1/Managers') + if status != 200: + return None + return wc + wc.set_basic_credentials(self.targuser, self.targpass) + rsp, status = wc.grab_json_response_with_status('/redfish/v1/Managers') + if status != 200: + return None + self.curruser = self.targuser + self.currpass = self.targpass + return wc + + def config(self, nodename): + self.nodename = nodename + creds = self.configmanager.get_node_attributes( + nodename, ['secret.hardwaremanagementuser', + 'secret.hardwaremanagementpassword', + 'hardwaremanagement.manager', 'hardwaremanagement.method', 'console.method'], + True) + cd = creds.get(nodename, {}) + defuser, defpass = self.get_firmware_default_account_info() + user, passwd, _ = self.get_node_credentials( + nodename, creds, defuser, defpass) + user = util.stringify(user) + passwd = util.stringify(passwd) + self.targuser = user + self.targpass = passwd + wc = self._get_wc() + srvroot, status = wc.grab_json_response_with_status('/redfish/v1/') + curruserinfo = {} + authupdate = {} + wc.set_header('Content-Type', 'application/json') + if user != self.curruser: + authupdate['UserName'] = user + if passwd != self.currpass: + authupdate['Password'] = passwd + if authupdate: + targaccturl = None + asrv = srvroot.get('AccountService', {}).get('@odata.id') + rsp, status = wc.grab_json_response_with_status(asrv) + accts = rsp.get('Accounts', {}).get('@odata.id') + rsp, status = wc.grab_json_response_with_status(accts) + accts = rsp.get('Members', []) + for accturl in accts: + accturl = accturl.get('@odata.id', '') + if accturl: + rsp, status = wc.grab_json_response_with_status(accturl) + if rsp.get('UserName', None) == self.curruser: + targaccturl = accturl + break + else: + raise Exception("Unable to identify Account URL to modify on this BMC") + rsp, status = wc.grab_json_response_with_status(targaccturl, authupdate, method='PATCH') + if status >= 300: + raise Exception("Failed attempting to update credentials on BMC") + wc.set_basic_credentials(user, passwd) + _, status = wc.grab_json_response_with_status('/redfish/v1/Managers') + tries = 10 + while tries and status >= 300: + tries -= 1 + eventlet.sleep(1.0) + _, status = wc.grab_json_response_with_status('/redfish/v1/Managers') + if ('hardwaremanagement.manager' in cd and + cd['hardwaremanagement.manager']['value'] and + not cd['hardwaremanagement.manager']['value'].startswith( + 'fe80::')): + newip = cd['hardwaremanagement.manager']['value'] + newip = newip.split('/', 1)[0] + newipinfo = getaddrinfo(newip, 0)[0] + newip = newipinfo[-1][0] + if ':' in newip: + raise exc.NotImplementedException('IPv6 remote config TODO') + mgrs = srvroot['Managers']['@odata.id'] + rsp = wc.grab_json_response(mgrs) + if len(rsp['Members']) != 1: + raise Exception("Can not handle multiple Managers") + mgrurl = rsp['Members'][0]['@odata.id'] + mginfo = wc.grab_json_response(mgrurl) + hifurls = get_host_interface_urls(wc, mginfo) + mgtnicinfo = mginfo['EthernetInterfaces']['@odata.id'] + mgtnicinfo = wc.grab_json_response(mgtnicinfo) + mgtnics = [x['@odata.id'] for x in mgtnicinfo.get('Members', [])] + actualnics = [] + for candnic in mgtnics: + if candnic in hifurls: + continue + actualnics.append(candnic) + if len(actualnics) != 1: + raise Exception("Multi-interface BMCs are not supported currently") + currnet = wc.grab_json_response(actualnics[0]) + netconfig = netutil.get_nic_config(self.configmanager, nodename, ip=newip) + newconfig = { + "Address": newip, + "SubnetMask": netutil.cidr_to_mask(netconfig['prefix']), + } + newgw = netconfig['ipv4_gateway'] + if newgw: + newconfig['Gateway'] = newgw + else: + newconfig['Gateway'] = newip # required property, set to self just to have a value + for net in currnet.get("IPv4Addresses", []): + if net["Address"] == newip and net["SubnetMask"] == newconfig['SubnetMask'] and (not newgw or newconfig['Gateway'] == newgw): + break + else: + wc.set_header('If-Match', '*') + rsp, status = wc.grab_json_response_with_status(actualnics[0], {'IPv4StaticAddresses': [newconfig]}, method='PATCH') + elif self.ipaddr.startswith('fe80::'): + self.configmanager.set_node_attributes( + {nodename: {'hardwaremanagement.manager': self.ipaddr}}) + else: + raise exc.TargetEndpointUnreachable( + 'hardwaremanagement.manager must be set to desired address (No IPv6 Link Local detected)') + + +def remote_nodecfg(nodename, cfm): + cfg = cfm.get_node_attributes( + nodename, 'hardwaremanagement.manager') + ipaddr = cfg.get(nodename, {}).get('hardwaremanagement.manager', {}).get( + 'value', None) + ipaddr = ipaddr.split('/', 1)[0] + ipaddr = getaddrinfo(ipaddr, 0)[0][-1] + if not ipaddr: + raise Exception('Cannot remote configure a system without known ' + 'address') + info = {'addresses': [ipaddr]} + nh = NodeHandler(info, cfm) + nh.config(nodename) + +if __name__ == '__main__': + import confluent.config.configmanager as cfm + c = cfm.ConfigManager(None) + import sys + info = {'addresses': [[sys.argv[1]]] } + print(repr(info)) + testr = NodeHandler(info, c) + testr.config(sys.argv[2]) diff --git a/confluent_server/confluent/discovery/protocols/ssdp.py b/confluent_server/confluent/discovery/protocols/ssdp.py index 3c1edc74..ec8275f1 100644 --- a/confluent_server/confluent/discovery/protocols/ssdp.py +++ b/confluent_server/confluent/discovery/protocols/ssdp.py @@ -60,6 +60,7 @@ def active_scan(handler, protocol=None): known_peers = set([]) for scanned in scan(['urn:dmtf-org:service:redfish-rest:1', 'urn::service:affluent']): for addr in scanned['addresses']: + addr = addr[0:1] + addr[2:] if addr in known_peers: break hwaddr = neighutil.get_hwaddr(addr[0]) @@ -79,13 +80,20 @@ def scan(services, target=None): def _process_snoop(peer, rsp, mac, known_peers, newmacs, peerbymacaddress, byehandler, machandlers, handler): - if mac in peerbymacaddress and peer not in peerbymacaddress[mac]['addresses']: - peerbymacaddress[mac]['addresses'].append(peer) + if mac in peerbymacaddress: + normpeer = peer[0:1] + peer[2:] + for currpeer in peerbymacaddress[mac]['addresses']: + currnormpeer = currpeer[0:1] + peer[2:] + if currnormpeer == normpeer: + break + else: + peerbymacaddress[mac]['addresses'].append(peer) else: peerdata = { 'hwaddr': mac, 'addresses': [peer], } + targurl = None for headline in rsp[1:]: if not headline: continue @@ -105,13 +113,20 @@ def _process_snoop(peer, rsp, mac, known_peers, newmacs, peerbymacaddress, byeha if not value.endswith('/redfish/v1/'): return elif header == 'LOCATION': - if not value.endswith('/DeviceDescription.json'): + if '/eth' in value and value.endswith('.xml'): + targurl = '/redfish/v1/' + targtype = 'megarac-bmc' + continue # MegaRAC redfish + elif value.endswith('/DeviceDescription.json'): + targurl = '/DeviceDescription.json' + targtype = 'megarac-bmc' + else: return - if handler: - eventlet.spawn_n(check_fish_handler, handler, peerdata, known_peers, newmacs, peerbymacaddress, machandlers, mac, peer) + if handler and targurl: + eventlet.spawn_n(check_fish_handler, handler, peerdata, known_peers, newmacs, peerbymacaddress, machandlers, mac, peer, targurl, targtype) -def check_fish_handler(handler, peerdata, known_peers, newmacs, peerbymacaddress, machandlers, mac, peer): - retdata = check_fish(('/DeviceDescription.json', peerdata)) +def check_fish_handler(handler, peerdata, known_peers, newmacs, peerbymacaddress, machandlers, mac, peer, targurl, targtype): + retdata = check_fish((targurl, peerdata, targtype)) if retdata: known_peers.add(peer) newmacs.add(mac) @@ -322,7 +337,7 @@ def _find_service(service, target): host = '[{0}]'.format(host) msg = smsg.format(host, service) if not isinstance(msg, bytes): - msg = msg.encode('utf8') + msg = msg.encode('utf8') net6.sendto(msg, addr[4]) else: net4.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) @@ -410,7 +425,11 @@ def _find_service(service, target): if '/redfish/v1/' not in peerdata[nid].get('urls', ()) and '/redfish/v1' not in peerdata[nid].get('urls', ()): continue if '/DeviceDescription.json' in peerdata[nid]['urls']: - pooltargs.append(('/DeviceDescription.json', peerdata[nid])) + pooltargs.append(('/DeviceDescription.json', peerdata[nid], 'lenovo-xcc')) + else: + for targurl in peerdata[nid]['urls']: + if '/eth' in targurl and targurl.endswith('.xml'): + pooltargs.append(('/redfish/v1/', peerdata[nid], 'megarac-bmc')) # For now, don't interrogate generic redfish bmcs # This is due to a need to deduplicate from some supported SLP # targets (IMM, TSM, others) @@ -425,7 +444,7 @@ def _find_service(service, target): def check_fish(urldata, port=443, verifycallback=None): if not verifycallback: verifycallback = lambda x: True - url, data = urldata + url, data, targtype = urldata try: wc = webclient.SecureHTTPConnection(_get_svrip(data), port, verifycallback=verifycallback, timeout=1.5) peerinfo = wc.grab_json_response(url) @@ -447,7 +466,7 @@ def check_fish(urldata, port=443, verifycallback=None): peerinfo = wc.grab_json_response('/redfish/v1/') if url == '/redfish/v1/': if 'UUID' in peerinfo: - data['services'] = ['service:redfish-bmc'] + data['services'] = [targtype] data['uuid'] = peerinfo['UUID'].lower() return data return None @@ -466,7 +485,12 @@ def _parse_ssdp(peer, rsp, peerdata): if code == b'200': if nid in peerdata: peerdatum = peerdata[nid] - if peer not in peerdatum['addresses']: + normpeer = peer[0:1] + peer[2:] + for currpeer in peerdatum['addresses']: + currnormpeer = currpeer[0:1] + peer[2:] + if currnormpeer == normpeer: + break + else: peerdatum['addresses'].append(peer) else: peerdatum = { @@ -501,5 +525,7 @@ def _parse_ssdp(peer, rsp, peerdata): if __name__ == '__main__': def printit(rsp): - print(repr(rsp)) + pass # print(repr(rsp)) active_scan(printit) + + From 9d979256eb2c8f96e6a2c334beb57a504eb30f02 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 27 Jun 2024 11:36:41 -0400 Subject: [PATCH 019/146] Revert "Add MegaRAC discovery support for recent MegaRAC" This reverts commit 07005d83ca09784b47903fb44f34d02aca48ec6e. Premature addition to master branch --- confluent_server/confluent/discovery/core.py | 9 +- .../confluent/discovery/handlers/megarac.py | 51 ---- .../discovery/handlers/redfishbmc.py | 269 ------------------ .../confluent/discovery/protocols/ssdp.py | 52 +--- 4 files changed, 15 insertions(+), 366 deletions(-) delete mode 100644 confluent_server/confluent/discovery/handlers/megarac.py delete mode 100644 confluent_server/confluent/discovery/handlers/redfishbmc.py diff --git a/confluent_server/confluent/discovery/core.py b/confluent_server/confluent/discovery/core.py index 7b94154a..dfb50b9f 100644 --- a/confluent_server/confluent/discovery/core.py +++ b/confluent_server/confluent/discovery/core.py @@ -74,7 +74,6 @@ import confluent.discovery.handlers.tsm as tsm import confluent.discovery.handlers.pxe as pxeh import confluent.discovery.handlers.smm as smm import confluent.discovery.handlers.xcc as xcc -import confluent.discovery.handlers.megarac as megarac import confluent.exceptions as exc import confluent.log as log import confluent.messages as msg @@ -114,7 +113,6 @@ nodehandlers = { 'service:lenovo-smm': smm, 'service:lenovo-smm2': smm, 'lenovo-xcc': xcc, - 'megarac-bmc': megarac, 'service:management-hardware.IBM:integrated-management-module2': imm, 'pxe-client': pxeh, 'onie-switch': None, @@ -134,7 +132,6 @@ servicenames = { 'service:lenovo-smm2': 'lenovo-smm2', 'affluent-switch': 'affluent-switch', 'lenovo-xcc': 'lenovo-xcc', - 'megarac-bmc': 'megarac-bmc', #'openbmc': 'openbmc', 'service:management-hardware.IBM:integrated-management-module2': 'lenovo-imm2', 'service:io-device.Lenovo:management-module': 'lenovo-switch', @@ -150,7 +147,6 @@ servicebyname = { 'lenovo-smm2': 'service:lenovo-smm2', 'affluent-switch': 'affluent-switch', 'lenovo-xcc': 'lenovo-xcc', - 'megarac-bmc': 'megarac-bmc', 'lenovo-imm2': 'service:management-hardware.IBM:integrated-management-module2', 'lenovo-switch': 'service:io-device.Lenovo:management-module', 'thinkagile-storage': 'service:thinkagile-storagebmc', @@ -457,7 +453,7 @@ def iterate_addrs(addrs, countonly=False): yield 1 return yield addrs - + def _parameterize_path(pathcomponents): listrequested = False childcoll = True @@ -546,7 +542,7 @@ def handle_api_request(configmanager, inputdata, operation, pathcomponents): if len(pathcomponents) > 2: raise Exception('TODO') currsubs = get_subscriptions() - return [msg.ChildCollection(x) for x in currsubs] + return [msg.ChildCollection(x) for x in currsubs] elif operation == 'retrieve': return handle_read_api_request(pathcomponents) elif (operation in ('update', 'create') and @@ -1707,4 +1703,3 @@ if __name__ == '__main__': start_detection() while True: eventlet.sleep(30) - diff --git a/confluent_server/confluent/discovery/handlers/megarac.py b/confluent_server/confluent/discovery/handlers/megarac.py deleted file mode 100644 index d7d8786a..00000000 --- a/confluent_server/confluent/discovery/handlers/megarac.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright 2024 Lenovo -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import confluent.discovery.handlers.redfishbmc as redfishbmc -import eventlet.support.greendns - - -getaddrinfo = eventlet.support.greendns.getaddrinfo - - -class NodeHandler(redfishbmc.NodeHandler): - - def get_firmware_default_account_info(self): - return ('admin', 'admin') - - -def remote_nodecfg(nodename, cfm): - cfg = cfm.get_node_attributes( - nodename, 'hardwaremanagement.manager') - ipaddr = cfg.get(nodename, {}).get('hardwaremanagement.manager', {}).get( - 'value', None) - ipaddr = ipaddr.split('/', 1)[0] - ipaddr = getaddrinfo(ipaddr, 0)[0][-1] - if not ipaddr: - raise Exception('Cannot remote configure a system without known ' - 'address') - info = {'addresses': [ipaddr]} - nh = NodeHandler(info, cfm) - nh.config(nodename) - - -if __name__ == '__main__': - import confluent.config.configmanager as cfm - c = cfm.ConfigManager(None) - import sys - info = {'addresses': [[sys.argv[1]]]} - print(repr(info)) - testr = NodeHandler(info, c) - testr.config(sys.argv[2]) - diff --git a/confluent_server/confluent/discovery/handlers/redfishbmc.py b/confluent_server/confluent/discovery/handlers/redfishbmc.py deleted file mode 100644 index eed401de..00000000 --- a/confluent_server/confluent/discovery/handlers/redfishbmc.py +++ /dev/null @@ -1,269 +0,0 @@ -# Copyright 2024 Lenovo -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import confluent.discovery.handlers.generic as generic -import confluent.exceptions as exc -import confluent.netutil as netutil -import confluent.util as util -import eventlet -import eventlet.support.greendns -import json -try: - from urllib import urlencode -except ImportError: - from urllib.parse import urlencode - -getaddrinfo = eventlet.support.greendns.getaddrinfo - -webclient = eventlet.import_patched('pyghmi.util.webclient') - -def get_host_interface_urls(wc, mginfo): - returls = [] - hifurl = mginfo.get('HostInterfaces', {}).get('@odata.id', None) - if not hifurl: - return None - hifinfo = wc.grab_json_response(hifurl) - hifurls = hifinfo.get('Members', []) - for hifurl in hifurls: - hifurl = hifurl['@odata.id'] - hifinfo = wc.grab_json_response(hifurl) - acturl = hifinfo.get('ManagerEthernetInterface', {}).get('@odata.id', None) - if acturl: - returls.append(acturl) - return returls - - -class NodeHandler(generic.NodeHandler): - devname = 'BMC' - - def __init__(self, info, configmanager): - self.trieddefault = None - self.targuser = None - self.curruser = None - self.currpass = None - self.targpass = None - self.nodename = None - self.csrftok = None - self.channel = None - self.atdefault = True - super(NodeHandler, self).__init__(info, configmanager) - - def get_firmware_default_account_info(self): - raise Exception('This must be subclassed') - - def scan(self): - c = webclient.SecureHTTPConnection(self.ipaddr, 443, verifycallback=self.validate_cert) - i = c.grab_json_response('/redfish/v1/') - uuid = i.get('UUID', None) - if uuid: - self.info['uuid'] = uuid.lower() - - def validate_cert(self, certificate): - # broadly speaking, merely checks consistency moment to moment, - # but if https_cert gets stricter, this check means something - fprint = util.get_fingerprint(self.https_cert) - return util.cert_matches(fprint, certificate) - - def _get_wc(self): - defuser, defpass = self.get_firmware_default_account_info() - wc = webclient.SecureHTTPConnection(self.ipaddr, 443, verifycallback=self.validate_cert) - wc.set_basic_credentials(defuser, defpass) - wc.set_header('Content-Type', 'application/json') - authmode = 0 - if not self.trieddefault: - rsp, status = wc.grab_json_response_with_status('/redfish/v1/Managers') - if status == 403: - self.trieddefault = True - chgurl = None - rsp = json.loads(rsp) - currerr = rsp.get('error', {}) - ecode = currerr.get('code', None) - if ecode.endswith('PasswordChangeRequired'): - for einfo in currerr.get('@Message.ExtendedInfo', []): - if einfo.get('MessageId', None).endswith('PasswordChangeRequired'): - for msgarg in einfo.get('MessageArgs'): - chgurl = msgarg - break - if chgurl: - if self.targpass == defpass: - raise Exception("Must specify a non-default password to onboard this BMC") - wc.set_header('If-Match', '*') - cpr = wc.grab_json_response_with_status(chgurl, {'Password': self.targpass}, method='PATCH') - if cpr[1] >= 200 and cpr[1] < 300: - self.curruser = defuser - self.currpass = self.targpass - wc.set_basic_credentials(self.curruser, self.currpass) - _, status = wc.grab_json_response_with_status('/redfish/v1/Managers') - tries = 10 - while status >= 300 and tries: - eventlet.sleep(1) - _, status = wc.grab_json_response_with_status('/redfish/v1/Managers') - return wc - - if status > 400: - self.trieddefault = True - if status == 401: - wc.set_basic_credentials(self.DEFAULT_USER, self.targpass) - rsp, status = wc.grab_json_response_with_status('/redfish/v1/Managers') - if status == 200: # Default user still, but targpass - self.currpass = self.targpass - self.curruser = defuser - return wc - elif self.targuser != defuser: - wc.set_basic_credentials(self.targuser, self.targpass) - rsp, status = wc.grab_json_response_with_status('/redfish/v1/Managers') - if status != 200: - raise Exception("Target BMC does not recognize firmware default credentials nor the confluent stored credential") - else: - self.curruser = defuser - self.currpass = defpass - return wc - if self.curruser: - wc.set_basic_credentials(self.curruser, self.currpass) - rsp, status = wc.grab_json_response_with_status('/redfish/v1/Managers') - if status != 200: - return None - return wc - wc.set_basic_credentials(self.targuser, self.targpass) - rsp, status = wc.grab_json_response_with_status('/redfish/v1/Managers') - if status != 200: - return None - self.curruser = self.targuser - self.currpass = self.targpass - return wc - - def config(self, nodename): - self.nodename = nodename - creds = self.configmanager.get_node_attributes( - nodename, ['secret.hardwaremanagementuser', - 'secret.hardwaremanagementpassword', - 'hardwaremanagement.manager', 'hardwaremanagement.method', 'console.method'], - True) - cd = creds.get(nodename, {}) - defuser, defpass = self.get_firmware_default_account_info() - user, passwd, _ = self.get_node_credentials( - nodename, creds, defuser, defpass) - user = util.stringify(user) - passwd = util.stringify(passwd) - self.targuser = user - self.targpass = passwd - wc = self._get_wc() - srvroot, status = wc.grab_json_response_with_status('/redfish/v1/') - curruserinfo = {} - authupdate = {} - wc.set_header('Content-Type', 'application/json') - if user != self.curruser: - authupdate['UserName'] = user - if passwd != self.currpass: - authupdate['Password'] = passwd - if authupdate: - targaccturl = None - asrv = srvroot.get('AccountService', {}).get('@odata.id') - rsp, status = wc.grab_json_response_with_status(asrv) - accts = rsp.get('Accounts', {}).get('@odata.id') - rsp, status = wc.grab_json_response_with_status(accts) - accts = rsp.get('Members', []) - for accturl in accts: - accturl = accturl.get('@odata.id', '') - if accturl: - rsp, status = wc.grab_json_response_with_status(accturl) - if rsp.get('UserName', None) == self.curruser: - targaccturl = accturl - break - else: - raise Exception("Unable to identify Account URL to modify on this BMC") - rsp, status = wc.grab_json_response_with_status(targaccturl, authupdate, method='PATCH') - if status >= 300: - raise Exception("Failed attempting to update credentials on BMC") - wc.set_basic_credentials(user, passwd) - _, status = wc.grab_json_response_with_status('/redfish/v1/Managers') - tries = 10 - while tries and status >= 300: - tries -= 1 - eventlet.sleep(1.0) - _, status = wc.grab_json_response_with_status('/redfish/v1/Managers') - if ('hardwaremanagement.manager' in cd and - cd['hardwaremanagement.manager']['value'] and - not cd['hardwaremanagement.manager']['value'].startswith( - 'fe80::')): - newip = cd['hardwaremanagement.manager']['value'] - newip = newip.split('/', 1)[0] - newipinfo = getaddrinfo(newip, 0)[0] - newip = newipinfo[-1][0] - if ':' in newip: - raise exc.NotImplementedException('IPv6 remote config TODO') - mgrs = srvroot['Managers']['@odata.id'] - rsp = wc.grab_json_response(mgrs) - if len(rsp['Members']) != 1: - raise Exception("Can not handle multiple Managers") - mgrurl = rsp['Members'][0]['@odata.id'] - mginfo = wc.grab_json_response(mgrurl) - hifurls = get_host_interface_urls(wc, mginfo) - mgtnicinfo = mginfo['EthernetInterfaces']['@odata.id'] - mgtnicinfo = wc.grab_json_response(mgtnicinfo) - mgtnics = [x['@odata.id'] for x in mgtnicinfo.get('Members', [])] - actualnics = [] - for candnic in mgtnics: - if candnic in hifurls: - continue - actualnics.append(candnic) - if len(actualnics) != 1: - raise Exception("Multi-interface BMCs are not supported currently") - currnet = wc.grab_json_response(actualnics[0]) - netconfig = netutil.get_nic_config(self.configmanager, nodename, ip=newip) - newconfig = { - "Address": newip, - "SubnetMask": netutil.cidr_to_mask(netconfig['prefix']), - } - newgw = netconfig['ipv4_gateway'] - if newgw: - newconfig['Gateway'] = newgw - else: - newconfig['Gateway'] = newip # required property, set to self just to have a value - for net in currnet.get("IPv4Addresses", []): - if net["Address"] == newip and net["SubnetMask"] == newconfig['SubnetMask'] and (not newgw or newconfig['Gateway'] == newgw): - break - else: - wc.set_header('If-Match', '*') - rsp, status = wc.grab_json_response_with_status(actualnics[0], {'IPv4StaticAddresses': [newconfig]}, method='PATCH') - elif self.ipaddr.startswith('fe80::'): - self.configmanager.set_node_attributes( - {nodename: {'hardwaremanagement.manager': self.ipaddr}}) - else: - raise exc.TargetEndpointUnreachable( - 'hardwaremanagement.manager must be set to desired address (No IPv6 Link Local detected)') - - -def remote_nodecfg(nodename, cfm): - cfg = cfm.get_node_attributes( - nodename, 'hardwaremanagement.manager') - ipaddr = cfg.get(nodename, {}).get('hardwaremanagement.manager', {}).get( - 'value', None) - ipaddr = ipaddr.split('/', 1)[0] - ipaddr = getaddrinfo(ipaddr, 0)[0][-1] - if not ipaddr: - raise Exception('Cannot remote configure a system without known ' - 'address') - info = {'addresses': [ipaddr]} - nh = NodeHandler(info, cfm) - nh.config(nodename) - -if __name__ == '__main__': - import confluent.config.configmanager as cfm - c = cfm.ConfigManager(None) - import sys - info = {'addresses': [[sys.argv[1]]] } - print(repr(info)) - testr = NodeHandler(info, c) - testr.config(sys.argv[2]) diff --git a/confluent_server/confluent/discovery/protocols/ssdp.py b/confluent_server/confluent/discovery/protocols/ssdp.py index ec8275f1..3c1edc74 100644 --- a/confluent_server/confluent/discovery/protocols/ssdp.py +++ b/confluent_server/confluent/discovery/protocols/ssdp.py @@ -60,7 +60,6 @@ def active_scan(handler, protocol=None): known_peers = set([]) for scanned in scan(['urn:dmtf-org:service:redfish-rest:1', 'urn::service:affluent']): for addr in scanned['addresses']: - addr = addr[0:1] + addr[2:] if addr in known_peers: break hwaddr = neighutil.get_hwaddr(addr[0]) @@ -80,20 +79,13 @@ def scan(services, target=None): def _process_snoop(peer, rsp, mac, known_peers, newmacs, peerbymacaddress, byehandler, machandlers, handler): - if mac in peerbymacaddress: - normpeer = peer[0:1] + peer[2:] - for currpeer in peerbymacaddress[mac]['addresses']: - currnormpeer = currpeer[0:1] + peer[2:] - if currnormpeer == normpeer: - break - else: - peerbymacaddress[mac]['addresses'].append(peer) + if mac in peerbymacaddress and peer not in peerbymacaddress[mac]['addresses']: + peerbymacaddress[mac]['addresses'].append(peer) else: peerdata = { 'hwaddr': mac, 'addresses': [peer], } - targurl = None for headline in rsp[1:]: if not headline: continue @@ -113,20 +105,13 @@ def _process_snoop(peer, rsp, mac, known_peers, newmacs, peerbymacaddress, byeha if not value.endswith('/redfish/v1/'): return elif header == 'LOCATION': - if '/eth' in value and value.endswith('.xml'): - targurl = '/redfish/v1/' - targtype = 'megarac-bmc' - continue # MegaRAC redfish - elif value.endswith('/DeviceDescription.json'): - targurl = '/DeviceDescription.json' - targtype = 'megarac-bmc' - else: + if not value.endswith('/DeviceDescription.json'): return - if handler and targurl: - eventlet.spawn_n(check_fish_handler, handler, peerdata, known_peers, newmacs, peerbymacaddress, machandlers, mac, peer, targurl, targtype) + if handler: + eventlet.spawn_n(check_fish_handler, handler, peerdata, known_peers, newmacs, peerbymacaddress, machandlers, mac, peer) -def check_fish_handler(handler, peerdata, known_peers, newmacs, peerbymacaddress, machandlers, mac, peer, targurl, targtype): - retdata = check_fish((targurl, peerdata, targtype)) +def check_fish_handler(handler, peerdata, known_peers, newmacs, peerbymacaddress, machandlers, mac, peer): + retdata = check_fish(('/DeviceDescription.json', peerdata)) if retdata: known_peers.add(peer) newmacs.add(mac) @@ -337,7 +322,7 @@ def _find_service(service, target): host = '[{0}]'.format(host) msg = smsg.format(host, service) if not isinstance(msg, bytes): - msg = msg.encode('utf8') + msg = msg.encode('utf8') net6.sendto(msg, addr[4]) else: net4.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) @@ -425,11 +410,7 @@ def _find_service(service, target): if '/redfish/v1/' not in peerdata[nid].get('urls', ()) and '/redfish/v1' not in peerdata[nid].get('urls', ()): continue if '/DeviceDescription.json' in peerdata[nid]['urls']: - pooltargs.append(('/DeviceDescription.json', peerdata[nid], 'lenovo-xcc')) - else: - for targurl in peerdata[nid]['urls']: - if '/eth' in targurl and targurl.endswith('.xml'): - pooltargs.append(('/redfish/v1/', peerdata[nid], 'megarac-bmc')) + pooltargs.append(('/DeviceDescription.json', peerdata[nid])) # For now, don't interrogate generic redfish bmcs # This is due to a need to deduplicate from some supported SLP # targets (IMM, TSM, others) @@ -444,7 +425,7 @@ def _find_service(service, target): def check_fish(urldata, port=443, verifycallback=None): if not verifycallback: verifycallback = lambda x: True - url, data, targtype = urldata + url, data = urldata try: wc = webclient.SecureHTTPConnection(_get_svrip(data), port, verifycallback=verifycallback, timeout=1.5) peerinfo = wc.grab_json_response(url) @@ -466,7 +447,7 @@ def check_fish(urldata, port=443, verifycallback=None): peerinfo = wc.grab_json_response('/redfish/v1/') if url == '/redfish/v1/': if 'UUID' in peerinfo: - data['services'] = [targtype] + data['services'] = ['service:redfish-bmc'] data['uuid'] = peerinfo['UUID'].lower() return data return None @@ -485,12 +466,7 @@ def _parse_ssdp(peer, rsp, peerdata): if code == b'200': if nid in peerdata: peerdatum = peerdata[nid] - normpeer = peer[0:1] + peer[2:] - for currpeer in peerdatum['addresses']: - currnormpeer = currpeer[0:1] + peer[2:] - if currnormpeer == normpeer: - break - else: + if peer not in peerdatum['addresses']: peerdatum['addresses'].append(peer) else: peerdatum = { @@ -525,7 +501,5 @@ def _parse_ssdp(peer, rsp, peerdata): if __name__ == '__main__': def printit(rsp): - pass # print(repr(rsp)) + print(repr(rsp)) active_scan(printit) - - From dde6ceadfbb3a62fe39981393ebbfb297e64f997 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 3 Jul 2024 14:36:54 -0400 Subject: [PATCH 020/146] Allow local ISO to proceed if detected with Ubuntu --- .../ubuntu22.04/initramfs/scripts/init-premount/confluent | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/confluent_osdeploy/ubuntu22.04/initramfs/scripts/init-premount/confluent b/confluent_osdeploy/ubuntu22.04/initramfs/scripts/init-premount/confluent index 03761f3a..8a7e3777 100755 --- a/confluent_osdeploy/ubuntu22.04/initramfs/scripts/init-premount/confluent +++ b/confluent_osdeploy/ubuntu22.04/initramfs/scripts/init-premount/confluent @@ -79,8 +79,12 @@ if [ ! -z "$cons" ]; then fi echo "Preparing to deploy $osprofile from $MGR" echo $osprofile > /custom-installation/confluent/osprofile -echo URL=http://${MGR}/confluent-public/os/$osprofile/distribution/install.iso >> /conf/param.conf -fcmdline="$(cat /custom-installation/confluent/cmdline.orig) url=http://${MGR}/confluent-public/os/$osprofile/distribution/install.iso" +. /etc/os-release +DIRECTISO=$(blkid -t TYPE=iso9660 |grep -Ei ' LABEL="Ubuntu-Server '$VERSION_ID) +if [ -z "$DIRECTISO" ]; then + echo URL=http://${MGR}/confluent-public/os/$osprofile/distribution/install.iso >> /conf/param.conf + fcmdline="$(cat /custom-installation/confluent/cmdline.orig) url=http://${MGR}/confluent-public/os/$osprofile/distribution/install.iso" +fi if [ ! -z "$cons" ]; then fcmdline="$fcmdline console=${cons#/dev/}" fi From 4f21e627054a0b09dcc2c0bdeea05f0897af04b9 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 8 Jul 2024 10:02:03 -0400 Subject: [PATCH 021/146] Fix typo in the auth code --- confluent_server/confluent/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_server/confluent/auth.py b/confluent_server/confluent/auth.py index fd07a133..fb82af24 100644 --- a/confluent_server/confluent/auth.py +++ b/confluent_server/confluent/auth.py @@ -58,7 +58,7 @@ _allowedbyrole = { '/nodes/', '/node*/media/uploads/', '/node*/inventory/firmware/updates/*', - '/node*/suppport/servicedata*', + '/node*/support/servicedata*', '/node*/attributes/expression', '/nodes/*/console/session*', '/nodes/*/shell/sessions*', From 7bd41af2ccca18b82f6ffeb95d7b7c4500b20937 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 9 Jul 2024 08:41:34 -0400 Subject: [PATCH 022/146] More properly error on bad requests Avoid incurring an error code 500 issue in reaction to certain bad request data. --- confluent_server/confluent/httpapi.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/confluent_server/confluent/httpapi.py b/confluent_server/confluent/httpapi.py index e30df36d..10ab8b86 100644 --- a/confluent_server/confluent/httpapi.py +++ b/confluent_server/confluent/httpapi.py @@ -175,6 +175,8 @@ def _get_query_dict(env, reqbody, reqtype): qstring = None if qstring: for qpair in qstring.split('&'): + if '=' not in qpair: + continue qkey, qvalue = qpair.split('=') qdict[qkey] = qvalue if reqbody is not None: @@ -668,7 +670,11 @@ def resourcehandler_backend(env, start_response): if 'CONTENT_LENGTH' in env and int(env['CONTENT_LENGTH']) > 0: reqbody = env['wsgi.input'].read(int(env['CONTENT_LENGTH'])) reqtype = env['CONTENT_TYPE'] - operation = opmap[env['REQUEST_METHOD']] + operation = opmap.get(env['REQUEST_METHOD'], None) + if not operation: + start_response('400 Bad Method', headers) + yield '' + return querydict = _get_query_dict(env, reqbody, reqtype) if operation != 'retrieve' and 'restexplorerop' in querydict: operation = querydict['restexplorerop'] From bc624d9360c06508abeff9f796e0a36224e2dde1 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 12 Jul 2024 15:15:56 -0400 Subject: [PATCH 023/146] Fix Ubuntu 24.04 network bring up Ubuntu 24.04 does not check conf files in /run before assuming dhcp anymore. Influence its logic to skip dhcp if we have static for it --- .../ubuntu22.04/initramfs/scripts/casper-bottom/99confluent | 2 ++ 1 file changed, 2 insertions(+) diff --git a/confluent_osdeploy/ubuntu22.04/initramfs/scripts/casper-bottom/99confluent b/confluent_osdeploy/ubuntu22.04/initramfs/scripts/casper-bottom/99confluent index e066714e..d629cf32 100755 --- a/confluent_osdeploy/ubuntu22.04/initramfs/scripts/casper-bottom/99confluent +++ b/confluent_osdeploy/ubuntu22.04/initramfs/scripts/casper-bottom/99confluent @@ -26,12 +26,14 @@ if [ -e /tmp/cnflnthmackeytmp ]; then chroot . curl -f -H "CONFLUENT_NODENAME: $NODENAME" -H "CONFLUENT_CRYPTHMAC: $(cat /root/$hmacfile)" -d @/tmp/cnflntcryptfile https://$MGR/confluent-api/self/registerapikey cp /root/$passfile /root/custom-installation/confluent/confluent.apikey DEVICE=$(cat /tmp/autodetectnic) + IP=done else chroot . custom-installation/confluent/bin/clortho $NODENAME $MGR > /root/custom-installation/confluent/confluent.apikey MGR=[$MGR] nic=$(grep ^MANAGER /custom-installation/confluent/confluent.info|grep fe80::|sed -e s/.*%//|head -n 1) nic=$(ip link |grep ^$nic:|awk '{print $2}') DEVICE=${nic%:} + IP=done fi if [ -z "$MGTIFACE" ]; then chroot . usr/bin/curl -f -H "CONFLUENT_NODENAME: $NODENAME" -H "CONFLUENT_APIKEY: $(cat /root//custom-installation/confluent/confluent.apikey)" https://${MGR}/confluent-api/self/deploycfg > $deploycfg From 8c193fe33f29831b3625b217fb0fe55f386f61da Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 12 Jul 2024 15:30:47 -0400 Subject: [PATCH 024/146] Fix issues with firstboot on Ubuntu 22+ --- .../ubuntu22.04/profiles/default/scripts/firstboot.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/firstboot.sh b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/firstboot.sh index c0ba44ab..996bfffe 100755 --- a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/firstboot.sh +++ b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/firstboot.sh @@ -3,11 +3,11 @@ echo "Confluent first boot is running" HOME=$(getent passwd $(whoami)|cut -d: -f 6) export HOME ( -exec >> /target/var/log/confluent/confluent-firstboot.log -exec 2>> /target/var/log/confluent/confluent-firstboot.log -chmod 600 /target/var/log/confluent/confluent-firstboot.log +exec >> /var/log/confluent/confluent-firstboot.log +exec 2>> /var/log/confluent/confluent-firstboot.log +chmod 600 /var/log/confluent/confluent-firstboot.log cp -a /etc/confluent/ssh/* /etc/ssh/ -systemctl restart sshd +systemctl restart ssh rootpw=$(grep ^rootpassword: /etc/confluent/confluent.deploycfg |awk '{print $2}') if [ ! -z "$rootpw" -a "$rootpw" != "null" ]; then echo root:$rootpw | chpasswd -e @@ -27,4 +27,4 @@ run_remote_parts firstboot.d run_remote_config firstboot.d curl --capath /etc/confluent/tls -f -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $confluent_apikey" -X POST -d "status: complete" https://$confluent_mgr/confluent-api/self/updatestatus ) & -tail --pid $! -n 0 -F /target/var/log/confluent/confluent-post.log > /dev/console +tail --pid $! -n 0 -F /var/log/confluent/confluent-post.log > /dev/console From 08a5bffa90486596b8fa392d9aba470d0e14e966 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 12 Jul 2024 15:52:49 -0400 Subject: [PATCH 025/146] Write multiple grub.cfg paths Some have different requirements on how to find grub.cfg --- confluent_server/confluent/osimage.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/confluent_server/confluent/osimage.py b/confluent_server/confluent/osimage.py index e0c1a8cb..3d5f50de 100644 --- a/confluent_server/confluent/osimage.py +++ b/confluent_server/confluent/osimage.py @@ -158,7 +158,7 @@ def find_glob(loc, fileglob): for cdir, _, fs in os.walk(loc): for f in fs: if fnmatch(f, fileglob): - return os.path.join(cdir, f) + return [os.path.join(cdir, f)] return None @@ -182,9 +182,13 @@ def update_boot_linux(profiledir, profile, label): # well need to honor grubprefix path if different grubcfgpath = find_glob(profiledir + '/boot', 'grub.cfg') if not grubcfgpath: - grubcfgpath = profiledir + '/boot/efi/boot/grub.cfg' - with open(grubcfgpath, 'w') as grubout: - grubout.write(grubcfg) + grubcfgpath = [ + profiledir + '/boot/efi/boot/grub.cfg' + profiledir + '/boot/boot/grub.cfg' + ] + for grubcfgpth in grubcfgpath: + with open(grubcfgpth, 'w') as grubout: + grubout.write(grubcfg) ipxeargs = kernelargs for initramfs in initrds: ipxeargs += " initrd=" + initramfs From 3aea1ec7d67082ece19f9306594b53ecc8dc52e8 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 12 Jul 2024 16:21:36 -0400 Subject: [PATCH 026/146] Fix list syntax in grub cfg --- confluent_server/confluent/osimage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_server/confluent/osimage.py b/confluent_server/confluent/osimage.py index 3d5f50de..5297424c 100644 --- a/confluent_server/confluent/osimage.py +++ b/confluent_server/confluent/osimage.py @@ -183,7 +183,7 @@ def update_boot_linux(profiledir, profile, label): grubcfgpath = find_glob(profiledir + '/boot', 'grub.cfg') if not grubcfgpath: grubcfgpath = [ - profiledir + '/boot/efi/boot/grub.cfg' + profiledir + '/boot/efi/boot/grub.cfg', profiledir + '/boot/boot/grub.cfg' ] for grubcfgpth in grubcfgpath: From c0cc673c63e551745f96380a453d29cf4065f408 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 12 Jul 2024 16:31:06 -0400 Subject: [PATCH 027/146] Make directory exist before creating file --- confluent_server/confluent/osimage.py | 1 + 1 file changed, 1 insertion(+) diff --git a/confluent_server/confluent/osimage.py b/confluent_server/confluent/osimage.py index 5297424c..5beb08f5 100644 --- a/confluent_server/confluent/osimage.py +++ b/confluent_server/confluent/osimage.py @@ -187,6 +187,7 @@ def update_boot_linux(profiledir, profile, label): profiledir + '/boot/boot/grub.cfg' ] for grubcfgpth in grubcfgpath: + os.makedirs(os.path.dirname(grubcfgpth), 0o755, exist_ok=True) with open(grubcfgpth, 'w') as grubout: grubout.write(grubcfg) ipxeargs = kernelargs From 7a3e1dfde3ed3e3c486c636c76f60c6fd12c5614 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 12 Jul 2024 16:48:46 -0400 Subject: [PATCH 028/146] Fix grub fallback path for more grub --- confluent_server/confluent/osimage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_server/confluent/osimage.py b/confluent_server/confluent/osimage.py index 5beb08f5..557cc99d 100644 --- a/confluent_server/confluent/osimage.py +++ b/confluent_server/confluent/osimage.py @@ -184,7 +184,7 @@ def update_boot_linux(profiledir, profile, label): if not grubcfgpath: grubcfgpath = [ profiledir + '/boot/efi/boot/grub.cfg', - profiledir + '/boot/boot/grub.cfg' + profiledir + '/boot/boot/grub/grub.cfg' ] for grubcfgpth in grubcfgpath: os.makedirs(os.path.dirname(grubcfgpth), 0o755, exist_ok=True) From 945dff09f34744c560ad9fed8b4a1d5a8a99c178 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 15 Jul 2024 08:19:13 -0400 Subject: [PATCH 029/146] Change to generic linux/inird command in Grub Modern grub has removed these variants, and should only be required for very old non-EFI stub kernels --- confluent_server/confluent/osimage.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/confluent_server/confluent/osimage.py b/confluent_server/confluent/osimage.py index 557cc99d..0e3d8a58 100644 --- a/confluent_server/confluent/osimage.py +++ b/confluent_server/confluent/osimage.py @@ -167,7 +167,7 @@ def update_boot_linux(profiledir, profile, label): kernelargs = profile.get('kernelargs', '') grubcfg = "set timeout=5\nmenuentry '" grubcfg += label - grubcfg += "' {\n linuxefi /kernel " + kernelargs + "\n" + grubcfg += "' {\n linux /kernel " + kernelargs + "\n" initrds = [] for initramfs in glob.glob(profiledir + '/boot/initramfs/*.cpio'): initramfs = os.path.basename(initramfs) @@ -175,7 +175,7 @@ def update_boot_linux(profiledir, profile, label): for initramfs in os.listdir(profiledir + '/boot/initramfs'): if initramfs not in initrds: initrds.append(initramfs) - grubcfg += " initrdefi " + grubcfg += " initrd " for initramfs in initrds: grubcfg += " /initramfs/{0}".format(initramfs) grubcfg += "\n}\n" From 8d726bced97a8f1bb45a3a433fdf22a03b515daf Mon Sep 17 00:00:00 2001 From: tkucherera Date: Mon, 15 Jul 2024 09:22:59 -0400 Subject: [PATCH 030/146] better error handling --- confluent_client/bin/nodebmcpassword | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/confluent_client/bin/nodebmcpassword b/confluent_client/bin/nodebmcpassword index f76b076c..135abb96 100755 --- a/confluent_client/bin/nodebmcpassword +++ b/confluent_client/bin/nodebmcpassword @@ -88,6 +88,7 @@ for rsp in session.read('/noderange/{0}/configuration/management_controller/user for node in databynode: if 'error' in rsp['databynode'][node]: print(node, ':', rsp['databynode'][node]['error']) + errorNodes.add(node) continue for user in rsp['databynode'][node]['users']: if user['username'] == username: @@ -97,6 +98,10 @@ for rsp in session.read('/noderange/{0}/configuration/management_controller/user uid_dict[user['uid']] = uid_dict[user['uid']] + ',{}'.format(node) break +if not uid_dict: + print("Error: Could not reach target node's bmc user") + sys.exit(1) + for uid in uid_dict: success = session.simple_noderange_command(uid_dict[uid], 'configuration/management_controller/users/{0}'.format(uid), new_password, key='password', errnodes=errorNodes) # = 0 if successful From abf12f2b962ca94550e321684228cd028e5f3618 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 15 Jul 2024 11:26:58 -0400 Subject: [PATCH 031/146] Reinstate linuxefi/initrdefi for older GRUB Technically, Grub never had 'linuxefi/initrdefi' commands officially, so this is a bit weird. However, if we see signs of GRUB older than 2.03, we will assume that is requires the linuxefi/initrdefi commands from the out of tree patch to support EFI the old way. This corresponds with EL7. Other variants seem ok with the more proper linux/initrd command names. --- confluent_server/confluent/osimage.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/confluent_server/confluent/osimage.py b/confluent_server/confluent/osimage.py index 0e3d8a58..5ff28d16 100644 --- a/confluent_server/confluent/osimage.py +++ b/confluent_server/confluent/osimage.py @@ -165,9 +165,27 @@ def find_glob(loc, fileglob): def update_boot_linux(profiledir, profile, label): profname = os.path.basename(profiledir) kernelargs = profile.get('kernelargs', '') + needefi = False + for grubexe in glob.glob(profiledir + '/boot/efi/boot/grubx64.efi'): + with open(grubexe, 'rb') as grubin: + grubcontent = grubin.read() + uaidx = grubcontent.find(b'User-Agent: GRUB 2.0') + if uaidx > 0: + grubcontent = grubcontent[uaidx:] + cridx = grubcontent.find(b'\r') + if cridx > 1: + grubcontent = grubcontent[:cridx] + grubver = grubcontent.split(b'~', 1)[0] + grubver = grubver.rsplit(b' ', 1)[-1] + grubver = grubver.split(b'.') + if len(grubver) > 1: + if int(grubver[0]) < 3 and int(grubver[1]) < 3: + needefi = True + lincmd = 'linuxefi' if needefi else 'linux' + initrdcmd = 'initrdefi' if needefi else 'initrd' grubcfg = "set timeout=5\nmenuentry '" grubcfg += label - grubcfg += "' {\n linux /kernel " + kernelargs + "\n" + grubcfg += "' {\n " + lincmd + " /kernel " + kernelargs + "\n" initrds = [] for initramfs in glob.glob(profiledir + '/boot/initramfs/*.cpio'): initramfs = os.path.basename(initramfs) @@ -175,7 +193,7 @@ def update_boot_linux(profiledir, profile, label): for initramfs in os.listdir(profiledir + '/boot/initramfs'): if initramfs not in initrds: initrds.append(initramfs) - grubcfg += " initrd " + grubcfg += " " + initrdcmd + " " for initramfs in initrds: grubcfg += " /initramfs/{0}".format(initramfs) grubcfg += "\n}\n" From 9d5432f8cd22d2bfa5a206b227b119d95f6a473e Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 18 Jul 2024 08:40:40 -0400 Subject: [PATCH 032/146] Fix network configuration when middle name ends in 'net' --- confluent_server/confluent/netutil.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_server/confluent/netutil.py b/confluent_server/confluent/netutil.py index 9e9fd597..9bac92c2 100644 --- a/confluent_server/confluent/netutil.py +++ b/confluent_server/confluent/netutil.py @@ -320,7 +320,7 @@ def get_full_net_config(configmanager, node, serverip=None): if val is None: continue if attrib.startswith('net.'): - attrib = attrib.replace('net.', '').rsplit('.', 1) + attrib = attrib.replace('net.', '', 1).rsplit('.', 1) if len(attrib) == 1: iface = None attrib = attrib[0] From b4a33b810230e2c37dce78ca6a1d659de576ec76 Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Thu, 18 Jul 2024 17:26:49 +0200 Subject: [PATCH 033/146] Fix EL stateful install Sometimes stateful install can fail if vgchange -a n is run after dd. Use wipefs instead and fix order of both commands. Furthermore, use the $INSALLDISK variable. --- confluent_osdeploy/el8/profiles/default/scripts/pre.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_osdeploy/el8/profiles/default/scripts/pre.sh b/confluent_osdeploy/el8/profiles/default/scripts/pre.sh index 4d76aaa3..cd831360 100644 --- a/confluent_osdeploy/el8/profiles/default/scripts/pre.sh +++ b/confluent_osdeploy/el8/profiles/default/scripts/pre.sh @@ -115,7 +115,7 @@ grep '^%include /tmp/partitioning' /tmp/kickstart.* > /dev/null || rm /tmp/insta if [ -e /tmp/installdisk -a ! -e /tmp/partitioning ]; then INSTALLDISK=$(cat /tmp/installdisk) sed -e s/%%INSTALLDISK%%/$INSTALLDISK/ -e s/%%LUKSHOOK%%/$LUKSPARTY/ /tmp/partitioning.template > /tmp/partitioning - dd if=/dev/zero of=/dev/$(cat /tmp/installdisk) bs=1M count=1 >& /dev/null vgchange -a n >& /dev/null + wipefs -a -f /dev/$INSTALLDISK >& /dev/null fi kill $logshowpid From 294ef8e88c0cd52942bca848092a6529ba05a927 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 19 Jul 2024 09:28:29 -0400 Subject: [PATCH 034/146] Fix for IB diskless boot to install clone The infiniband section must be defined for the OS to use the IB link. If it is missing then networking does not come up during firstboot. Fix this by having an inifiniband section including explicitly declaring use of datagram mode. This should suffice for all install use cases, and may be changed after firstboot starts. --- .../usr/lib/dracut/hooks/cmdline/10-confluentdiskless.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/confluent_osdeploy/el9-diskless/initramfs/usr/lib/dracut/hooks/cmdline/10-confluentdiskless.sh b/confluent_osdeploy/el9-diskless/initramfs/usr/lib/dracut/hooks/cmdline/10-confluentdiskless.sh index a4f10ee2..9b885e82 100644 --- a/confluent_osdeploy/el9-diskless/initramfs/usr/lib/dracut/hooks/cmdline/10-confluentdiskless.sh +++ b/confluent_osdeploy/el9-diskless/initramfs/usr/lib/dracut/hooks/cmdline/10-confluentdiskless.sh @@ -171,6 +171,13 @@ permissions= wait-device-timeout=60000 EOC +if [ "$linktype" = infiniband ]; then +cat >> /run/NetworkManager/system-connections/$ifname.nmconnection << EOC +[infiniband] +transport-mode=datagram + +EOC +fi autoconfigmethod=$(grep ^ipv4_method: /etc/confluent/confluent.deploycfg |awk '{print $2}') auto6configmethod=$(grep ^ipv6_method: /etc/confluent/confluent.deploycfg |awk '{print $2}') if [ "$autoconfigmethod" = "dhcp" ]; then From ede941c0d91d5bd00229f1b28998097e9881530d Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 22 Jul 2024 13:46:27 -0400 Subject: [PATCH 035/146] Add deb packaging of imgutil --- imgutil/builddeb | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100755 imgutil/builddeb diff --git a/imgutil/builddeb b/imgutil/builddeb new file mode 100755 index 00000000..4258fbc4 --- /dev/null +++ b/imgutil/builddeb @@ -0,0 +1,24 @@ +#!/bin/bash +VERSION=`git describe|cut -d- -f 1` +NUMCOMMITS=`git describe|cut -d- -f 2` +if [ "$NUMCOMMITS" != "$VERSION" ]; then + VERSION=$VERSION.dev$NUMCOMMITS.g`git describe|cut -d- -f 3` +fi +mkdir -p /tmp/confluent-imgutil +cp -a * /tmp/confluent-imgutil +cp ../LICENSE /tmp/confluent-imgutil +cd /tmp/confluent-imgutil +rm -rf deb/confluent_imgutil_$VERSION/ +mkdir -p deb/confluent_imgutil_$VERSION/DEBIAN/ +mkdir -p deb/confluent_imgutil_$VERSION/opt/confluent/lib/imgutil +mkdir -p deb/confluent_imgutil_$VERSION/opt/confluent/bin +mv imgutil deb/confluent_imgutil_$VERSION/opt/confluent/bin/ +chmod a+x deb/confluent_imgutil_$VERSION/opt/confluent/bin/imgutil +mv ubuntu* suse15 el7 el9 el8 deb/confluent_imgutil_$VERSION/opt/confluent/lib/imgutil/ +mkdir -p deb/confluent_imgutil_$VERSION/opt/confluent/share/licenses/confluent_imgutil +cp LICENSE deb/confluent_imgutil_$VERSION/opt/confluent/share/licenses/confluent_imgutil +sed -e 's/#VERSION#/'$VERSION/ control.tmpl > deb/confluent_imgutil_$VERSION/DEBIAN/control +dpkg-deb --build deb/lenovo_confluent_$VERSION +if [ ! -z "$1" ]; then + mv deb/lenovo-confluent_$VERSION.deb $1 +fi From 36ca68f44dba1bb028fc32a0aa4f17e8c9154a0a Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 22 Jul 2024 13:46:45 -0400 Subject: [PATCH 036/146] Add control file for deb build of imgutil --- imgutil/control.tmpl | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 imgutil/control.tmpl diff --git a/imgutil/control.tmpl b/imgutil/control.tmpl new file mode 100644 index 00000000..a0fe21af --- /dev/null +++ b/imgutil/control.tmpl @@ -0,0 +1,8 @@ +Package: confluent-imgutil +Version: #VERSION# +Section: base +Priority: optional +Maintainer: Jarrod Johnson +Description: Web frontend for confluent server +Architecture: all + From 69fa3f10c0f38cf7a6fc52d13ed2c0fd5d2e41f9 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 22 Jul 2024 13:46:27 -0400 Subject: [PATCH 037/146] Add deb packaging of imgutil --- imgutil/builddeb | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100755 imgutil/builddeb diff --git a/imgutil/builddeb b/imgutil/builddeb new file mode 100755 index 00000000..4258fbc4 --- /dev/null +++ b/imgutil/builddeb @@ -0,0 +1,24 @@ +#!/bin/bash +VERSION=`git describe|cut -d- -f 1` +NUMCOMMITS=`git describe|cut -d- -f 2` +if [ "$NUMCOMMITS" != "$VERSION" ]; then + VERSION=$VERSION.dev$NUMCOMMITS.g`git describe|cut -d- -f 3` +fi +mkdir -p /tmp/confluent-imgutil +cp -a * /tmp/confluent-imgutil +cp ../LICENSE /tmp/confluent-imgutil +cd /tmp/confluent-imgutil +rm -rf deb/confluent_imgutil_$VERSION/ +mkdir -p deb/confluent_imgutil_$VERSION/DEBIAN/ +mkdir -p deb/confluent_imgutil_$VERSION/opt/confluent/lib/imgutil +mkdir -p deb/confluent_imgutil_$VERSION/opt/confluent/bin +mv imgutil deb/confluent_imgutil_$VERSION/opt/confluent/bin/ +chmod a+x deb/confluent_imgutil_$VERSION/opt/confluent/bin/imgutil +mv ubuntu* suse15 el7 el9 el8 deb/confluent_imgutil_$VERSION/opt/confluent/lib/imgutil/ +mkdir -p deb/confluent_imgutil_$VERSION/opt/confluent/share/licenses/confluent_imgutil +cp LICENSE deb/confluent_imgutil_$VERSION/opt/confluent/share/licenses/confluent_imgutil +sed -e 's/#VERSION#/'$VERSION/ control.tmpl > deb/confluent_imgutil_$VERSION/DEBIAN/control +dpkg-deb --build deb/lenovo_confluent_$VERSION +if [ ! -z "$1" ]; then + mv deb/lenovo-confluent_$VERSION.deb $1 +fi From 7154a1d60ca641d8ea44128ece8432d89ca4bfeb Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 22 Jul 2024 13:46:45 -0400 Subject: [PATCH 038/146] Add control file for deb build of imgutil --- imgutil/control.tmpl | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 imgutil/control.tmpl diff --git a/imgutil/control.tmpl b/imgutil/control.tmpl new file mode 100644 index 00000000..a0fe21af --- /dev/null +++ b/imgutil/control.tmpl @@ -0,0 +1,8 @@ +Package: confluent-imgutil +Version: #VERSION# +Section: base +Priority: optional +Maintainer: Jarrod Johnson +Description: Web frontend for confluent server +Architecture: all + From 4f18294d93f281af4eaa088a1a29317cf10c8574 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 22 Jul 2024 13:57:38 -0400 Subject: [PATCH 039/146] Fix path in debian build for imgutil --- imgutil/builddeb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/imgutil/builddeb b/imgutil/builddeb index 4258fbc4..7e12a6e6 100755 --- a/imgutil/builddeb +++ b/imgutil/builddeb @@ -18,7 +18,7 @@ mv ubuntu* suse15 el7 el9 el8 deb/confluent_imgutil_$VERSION/opt/confluent/lib/i mkdir -p deb/confluent_imgutil_$VERSION/opt/confluent/share/licenses/confluent_imgutil cp LICENSE deb/confluent_imgutil_$VERSION/opt/confluent/share/licenses/confluent_imgutil sed -e 's/#VERSION#/'$VERSION/ control.tmpl > deb/confluent_imgutil_$VERSION/DEBIAN/control -dpkg-deb --build deb/lenovo_confluent_$VERSION +dpkg-deb --build deb/confluent_imgutil_$VERSION if [ ! -z "$1" ]; then - mv deb/lenovo-confluent_$VERSION.deb $1 + mv deb/confluent_imgutil_$VERSION.deb $1 fi From 34b03da4941210cd8ad19ae7852cb7d390eafd82 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 22 Jul 2024 16:33:07 -0400 Subject: [PATCH 040/146] Update for Ubuntu 24.04 diskless --- imgutil/imgutil | 1 + imgutil/ubuntu24.04 | 1 + 2 files changed, 2 insertions(+) create mode 120000 imgutil/ubuntu24.04 diff --git a/imgutil/imgutil b/imgutil/imgutil index 022279cc..d5714306 100644 --- a/imgutil/imgutil +++ b/imgutil/imgutil @@ -655,6 +655,7 @@ class DebHandler(OsHandler): def prep_root(self, args): shutil.copy('/etc/apt/sources.list', os.path.join(self.targpath, 'etc/apt/sources.list')) + shutil.copytree('/etc/apt/sources.list.d', os.path.join(self.targpath, 'etc/apt/sources.list.d')) args.cmd = ['apt-get', 'update'] run_constrainedx(fancy_chroot, (args, self.targpath)) args.cmd = ['apt-get', '-y', 'install'] + self.includepkgs diff --git a/imgutil/ubuntu24.04 b/imgutil/ubuntu24.04 new file mode 120000 index 00000000..7d13753d --- /dev/null +++ b/imgutil/ubuntu24.04 @@ -0,0 +1 @@ +ubuntu \ No newline at end of file From 1ade704daa4bddb98481ab502d815d9df33bae92 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 22 Jul 2024 16:40:44 -0400 Subject: [PATCH 041/146] Fix imgutil copy of ubuntu sources --- imgutil/imgutil | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/imgutil/imgutil b/imgutil/imgutil index d5714306..1c6d3cc0 100644 --- a/imgutil/imgutil +++ b/imgutil/imgutil @@ -655,7 +655,8 @@ class DebHandler(OsHandler): def prep_root(self, args): shutil.copy('/etc/apt/sources.list', os.path.join(self.targpath, 'etc/apt/sources.list')) - shutil.copytree('/etc/apt/sources.list.d', os.path.join(self.targpath, 'etc/apt/sources.list.d')) + for listfile in glob.glob('/etc/apt/sources.list.d/*'): + shutil.copy(listfile, os.path.join(self.targpath, listfile[1:])) args.cmd = ['apt-get', 'update'] run_constrainedx(fancy_chroot, (args, self.targpath)) args.cmd = ['apt-get', '-y', 'install'] + self.includepkgs From 33ed1a5e640802deccfaefbab894770ac53b4667 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 23 Jul 2024 09:32:20 -0400 Subject: [PATCH 042/146] Add onboot for ubuntu diskless --- .../profiles/default/scripts/onboot.service | 11 +++++ .../profiles/default/scripts/onboot.sh | 40 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 confluent_osdeploy/ubuntu20.04-diskless/profiles/default/scripts/onboot.service create mode 100644 confluent_osdeploy/ubuntu20.04-diskless/profiles/default/scripts/onboot.sh diff --git a/confluent_osdeploy/ubuntu20.04-diskless/profiles/default/scripts/onboot.service b/confluent_osdeploy/ubuntu20.04-diskless/profiles/default/scripts/onboot.service new file mode 100644 index 00000000..f9235033 --- /dev/null +++ b/confluent_osdeploy/ubuntu20.04-diskless/profiles/default/scripts/onboot.service @@ -0,0 +1,11 @@ +[Unit] +Description=Confluent onboot hook +Requires=network-online.target +After=network-online.target + +[Service] +ExecStart=/opt/confluent/bin/onboot.sh + +[Install] +WantedBy=multi-user.target + diff --git a/confluent_osdeploy/ubuntu20.04-diskless/profiles/default/scripts/onboot.sh b/confluent_osdeploy/ubuntu20.04-diskless/profiles/default/scripts/onboot.sh new file mode 100644 index 00000000..60ccaa44 --- /dev/null +++ b/confluent_osdeploy/ubuntu20.04-diskless/profiles/default/scripts/onboot.sh @@ -0,0 +1,40 @@ +#!/bin/sh + +# This script is executed on each boot as it is +# completed. It is best to edit the middle of the file as +# noted below so custom commands are executed before +# the script notifies confluent that install is fully complete. + +nodename=$(grep ^NODENAME /etc/confluent/confluent.info|awk '{print $2}') +confluent_apikey=$(cat /etc/confluent/confluent.apikey) +v4meth=$(grep ^ipv4_method: /etc/confluent/confluent.deploycfg|awk '{print $2}') +if [ "$v4meth" = "null" -o -z "$v4meth" ]; then + confluent_mgr=$(grep ^deploy_server_v6: /etc/confluent/confluent.deploycfg|awk '{print $2}') +fi +if [ -z "$confluent_mgr" ]; then + confluent_mgr=$(grep ^deploy_server: /etc/confluent/confluent.deploycfg|awk '{print $2}') +fi +confluent_profile=$(grep ^profile: /etc/confluent/confluent.deploycfg|awk '{print $2}') +timedatectl set-timezone $(grep ^timezone: /etc/confluent/confluent.deploycfg|awk '{print $2}') +hostnamectl set-hostname $nodename +export nodename confluent_mgr confluent_profile +. /etc/confluent/functions +mkdir -p /var/log/confluent +chmod 700 /var/log/confluent +exec >> /var/log/confluent/confluent-onboot.log +exec 2>> /var/log/confluent/confluent-onboot.log +chmod 600 /var/log/confluent/confluent-onboot.log +tail -f /var/log/confluent/confluent-onboot.log > /dev/console & +logshowpid=$! + +run_remote_python syncfileclient +run_remote_python confignet + +# onboot scripts may be placed into onboot.d, e.g. onboot.d/01-firstaction.sh, onboot.d/02-secondaction.sh +run_remote_parts onboot.d + +# Induce execution of remote configuration, e.g. ansible plays in ansible/onboot.d/ +run_remote_config onboot.d + +#curl -X POST -d 'status: booted' -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $confluent_apikey" https://$confluent_mgr/confluent-api/self/updatestatus +kill $logshowpid From bb04faed04c28c92e49723f592dbd0c2a5df278d Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 23 Jul 2024 10:01:53 -0400 Subject: [PATCH 043/146] Explicitly request bash under ubuntu, which tends to use dash --- .../ubuntu20.04-diskless/profiles/default/scripts/onboot.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_osdeploy/ubuntu20.04-diskless/profiles/default/scripts/onboot.sh b/confluent_osdeploy/ubuntu20.04-diskless/profiles/default/scripts/onboot.sh index 60ccaa44..cc470d6f 100644 --- a/confluent_osdeploy/ubuntu20.04-diskless/profiles/default/scripts/onboot.sh +++ b/confluent_osdeploy/ubuntu20.04-diskless/profiles/default/scripts/onboot.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash # This script is executed on each boot as it is # completed. It is best to edit the middle of the file as From a94b9235e8ddf6941a58d9f576b7ec67e4a58613 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 23 Jul 2024 10:14:32 -0400 Subject: [PATCH 044/146] Tighten umask on confignet to avoid ubuntu warnings --- confluent_osdeploy/common/profile/scripts/confignet | 2 ++ 1 file changed, 2 insertions(+) diff --git a/confluent_osdeploy/common/profile/scripts/confignet b/confluent_osdeploy/common/profile/scripts/confignet index 8cda6c83..72462834 100644 --- a/confluent_osdeploy/common/profile/scripts/confignet +++ b/confluent_osdeploy/common/profile/scripts/confignet @@ -192,8 +192,10 @@ class NetplanManager(object): if needcfgwrite: needcfgapply = True newcfg = {'network': {'version': 2, 'ethernets': {devname: self.cfgbydev[devname]}}} + oumask = os.umask(0o77) with open('/etc/netplan/{0}-confluentcfg.yaml'.format(devname), 'w') as planout: planout.write(yaml.dump(newcfg)) + os.umask(oumask) if needcfgapply: subprocess.call(['netplan', 'apply']) From cf4475cfccbc84425fee513ec25c96c26a7bf0a0 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 23 Jul 2024 10:23:05 -0400 Subject: [PATCH 045/146] Escape the '\W' to avoid stepping on python processing --- imgutil/imgutil | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imgutil/imgutil b/imgutil/imgutil index 1c6d3cc0..4ef06776 100644 --- a/imgutil/imgutil +++ b/imgutil/imgutil @@ -947,7 +947,7 @@ def fancy_chroot(args, installroot): os.chroot(installroot) os.chdir('/') _mount('/', '/', flags=MS_BIND) # Make / manifest as a mounted filesystem in exec - os.environ['PS1'] = '[\x1b[1m\x1b[4mIMGUTIL EXEC {0}\x1b[0m \W]$ '.format(imgname) + os.environ['PS1'] = '[\x1b[1m\x1b[4mIMGUTIL EXEC {0}\x1b[0m \\W]$ '.format(imgname) os.environ['CONFLUENT_IMGUTIL_MODE'] = 'exec' if oshandler: oshandler.set_source('/run/confluentdistro') From 8f58567a700e0283ee681acd7a790bc917a0c693 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 23 Jul 2024 11:05:51 -0400 Subject: [PATCH 046/146] Add ssh to default services of a built ubuntu image --- imgutil/imgutil | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/imgutil/imgutil b/imgutil/imgutil index 4ef06776..907a3b64 100644 --- a/imgutil/imgutil +++ b/imgutil/imgutil @@ -661,6 +661,12 @@ class DebHandler(OsHandler): run_constrainedx(fancy_chroot, (args, self.targpath)) args.cmd = ['apt-get', '-y', 'install'] + self.includepkgs run_constrainedx(fancy_chroot, (args, self.targpath)) + servicefile = os.path.join(self.targpath, 'usr/lib/systemd/system/ssh.service') + if os.path.exists(servicefile): + os.symlink('/usr/lib/systemd/system/ssh.service', os.path.join(self.targpath, 'etc/systemd/system/multi-user.target.wants/ssh.service')) + else: + os.symlink('/usr/lib/systemd/system/sshd.service', os.path.join(self.targpath, 'etc/systemd/system/multi-user.target.wants/sshd.service')) + class ElHandler(OsHandler): From 2235faa76dea31f1a455c1f2208bdd2e086b8be4 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 24 Jul 2024 08:33:20 -0400 Subject: [PATCH 047/146] Stop using private interface of PyCA PyCA changes their minds about which bindings to include. So make the binding ourselves since PyCA removed it in certain versions. This is a backport of the implementation from the async port effort. --- confluent_server/confluent/sockapi.py | 31 ++++++++++++++++++++------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/confluent_server/confluent/sockapi.py b/confluent_server/confluent/sockapi.py index 2d4db15b..8aca0058 100644 --- a/confluent_server/confluent/sockapi.py +++ b/confluent_server/confluent/sockapi.py @@ -70,15 +70,17 @@ try: # so we need to ffi that in using a strategy compatible with PyOpenSSL import OpenSSL.SSL as libssln import OpenSSL.crypto as crypto - from OpenSSL._util import ffi except ImportError: libssl = None - ffi = None crypto = None plainsocket = None libc = ctypes.CDLL(ctypes.util.find_library('c')) +libsslc = ctypes.CDLL(ctypes.util.find_library('ssl')) +libsslc.SSL_CTX_set_cert_verify_callback.argtypes = [ + ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] + def _should_authlog(path, operation): if (operation == 'retrieve' and @@ -389,11 +391,24 @@ def _tlshandler(bind_host, bind_port): else: eventlet.spawn_n(_tlsstartup, cnn) +@ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p) +def verify_stub(store, misc): + return 1 + +class PyObject_HEAD(ctypes.Structure): + _fields_ = [ + ("ob_refcnt", ctypes.c_ssize_t), + ("ob_type", ctypes.c_void_p), + ] + + +# see main/Modules/_ssl.c, only caring about the SSL_CTX pointer +class PySSLContext(ctypes.Structure): + _fields_ = [ + ("ob_base", PyObject_HEAD), + ("ctx", ctypes.c_void_p), + ] -if ffi: - @ffi.callback("int(*)( X509_STORE_CTX *, void*)") - def verify_stub(store, misc): - return 1 def _tlsstartup(cnn): @@ -416,8 +431,8 @@ def _tlsstartup(cnn): ctx.use_certificate_file('/etc/confluent/srvcert.pem') ctx.use_privatekey_file('/etc/confluent/privkey.pem') ctx.set_verify(libssln.VERIFY_PEER, lambda *args: True) - libssln._lib.SSL_CTX_set_cert_verify_callback(ctx._context, - verify_stub, ffi.NULL) + ssl_ctx = PySSLContext.from_address(id(ctx)).ctx + libsslc.SSL_CTX_set_cert_verify_callback(ssl_ctx, verify_stub, 0) cnn = libssl.Connection(ctx, cnn) cnn.set_accept_state() cnn.do_handshake() From c91af840e510ccbed54a858a97623792398aa1ee Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 24 Jul 2024 11:12:31 -0400 Subject: [PATCH 048/146] Robust handling of relative link resolv.conf resolv.conf may be a relative link, normal file, or absolute link. Handle all cases. --- imgutil/imgutil | 2 ++ 1 file changed, 2 insertions(+) diff --git a/imgutil/imgutil b/imgutil/imgutil index 907a3b64..bc34af01 100644 --- a/imgutil/imgutil +++ b/imgutil/imgutil @@ -942,6 +942,8 @@ def fancy_chroot(args, installroot): sourceresolv = '/etc/resolv.conf' if os.path.islink(sourceresolv): sourceresolv = os.readlink(sourceresolv) + # normalize and resolve relative and absolute paths + sourceresolv = os.path.normpath(os.path.join('/etc', sourceresolv)) dstresolv = os.path.join(installroot, 'etc/resolv.conf') if os.path.islink(dstresolv): dstresolv = os.path.join(installroot, os.readlink(dstresolv)[1:]) From 714fefe31bbff6d5e3fee989707554dc59daa2a7 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 24 Jul 2024 14:41:39 -0400 Subject: [PATCH 049/146] Fix unethered boot for ubuntu --- .../ubuntu20.04-diskless/profiles/default/scripts/imageboot.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_osdeploy/ubuntu20.04-diskless/profiles/default/scripts/imageboot.sh b/confluent_osdeploy/ubuntu20.04-diskless/profiles/default/scripts/imageboot.sh index f1b8e45a..ff8d253d 100644 --- a/confluent_osdeploy/ubuntu20.04-diskless/profiles/default/scripts/imageboot.sh +++ b/confluent_osdeploy/ubuntu20.04-diskless/profiles/default/scripts/imageboot.sh @@ -8,7 +8,7 @@ for addr in $(grep ^MANAGER: /etc/confluent/confluent.info|awk '{print $2}'|sed fi done mkdir -p /mnt/remoteimg /mnt/remote /mnt/overlay -if grep confluennt_imagemethtod=untethered /proc/cmdline > /dev/null; then +if grep confluennt_imagemethod=untethered /proc/cmdline > /dev/null; then mount -t tmpfs untethered /mnt/remoteimg curl https://$confluent_mgr/confluent-public/os/$confluent_profile/rootimg.sfs -o /mnt/remoteimg/rootimg.sfs else From a92edc7924fbc6f593bf05e07cb7d8d38d155050 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 24 Jul 2024 15:20:02 -0400 Subject: [PATCH 050/146] Apply ownership sanity check even for root User could accidently run 'confluent' in a way that makes no sense, block it the most accessible way. The pid file should have blocked it, but systemd purges the directory even on failure. --- confluent_server/confluent/main.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/confluent_server/confluent/main.py b/confluent_server/confluent/main.py index b49d8f56..9fb27972 100644 --- a/confluent_server/confluent/main.py +++ b/confluent_server/confluent/main.py @@ -220,16 +220,20 @@ def setlimits(): def assure_ownership(path): try: if os.getuid() != os.stat(path).st_uid: - sys.stderr.write('{} is not owned by confluent user, change ownership\n'.format(path)) + if os.getuid() == 0: + sys.stderr.write('Attempting to run as root, when non-root usage is detected\n') + else: + sys.stderr.write('{} is not owned by confluent user, change ownership\n'.format(path)) sys.exit(1) except OSError as e: if e.errno == 13: - sys.stderr.write('{} is not owned by confluent user, change ownership\n'.format(path)) + if os.getuid() == 0: + sys.stderr.write('Attempting to run as root, when non-root usage is detected\n') + else: + sys.stderr.write('{} is not owned by confluent user, change ownership\n'.format(path)) sys.exit(1) def sanity_check(): - if os.getuid() == 0: - return True assure_ownership('/etc/confluent') assure_ownership('/etc/confluent/cfg') for filename in glob.glob('/etc/confluent/cfg/*'): From 6e8d8dabd11e5821d8c34a38803e631c58792c58 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 24 Jul 2024 15:28:03 -0400 Subject: [PATCH 051/146] Fix whitespace issue --- confluent_server/confluent/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_server/confluent/main.py b/confluent_server/confluent/main.py index 9fb27972..b0e3508a 100644 --- a/confluent_server/confluent/main.py +++ b/confluent_server/confluent/main.py @@ -227,7 +227,7 @@ def assure_ownership(path): sys.exit(1) except OSError as e: if e.errno == 13: - if os.getuid() == 0: + if os.getuid() == 0: sys.stderr.write('Attempting to run as root, when non-root usage is detected\n') else: sys.stderr.write('{} is not owned by confluent user, change ownership\n'.format(path)) From 8f1a1130a8b9348aa682925ec9cda1871238bb70 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 24 Jul 2024 15:55:04 -0400 Subject: [PATCH 052/146] Add a selfcheck to check misdone collective manager --- confluent_server/bin/confluent_selfcheck | 27 +++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/confluent_server/bin/confluent_selfcheck b/confluent_server/bin/confluent_selfcheck index b9651d17..64794ae4 100755 --- a/confluent_server/bin/confluent_selfcheck +++ b/confluent_server/bin/confluent_selfcheck @@ -24,6 +24,9 @@ import eventlet import greenlet import pwd import signal +import confluent.collective.manager as collective +import confluent.noderange as noderange + def fprint(txt): sys.stdout.write(txt) @@ -258,6 +261,9 @@ if __name__ == '__main__': uuid = rsp.get('id.uuid', {}).get('value', None) if uuid: uuidok = True + if 'collective.managercandidates' in rsp: + # Check if current node in candidates + pass if 'deployment.useinsecureprotocols' in rsp: insec = rsp.get('deployment.useinsecureprotocols', {}).get('value', None) if insec != 'firmware': @@ -276,8 +282,27 @@ if __name__ == '__main__': switch_value = rsp[key].get('value',None) if switch_value and switch_value not in valid_nodes: emprint(f'{switch_value} is not a valid node name (as referenced by attribute "{key}" of node {args.node}).') - print(f"Checking network configuration for {args.node}") cfg = configmanager.ConfigManager(None) + cfd = cfg.get_node_attributes( + args.node, ('deployment.*', 'collective.managercandidates')) + profile = cfd.get(args.node, {}).get( + 'deployment.pendingprofile', {}).get('value', None) + if not profile: + emprint( + f'{args.node} is not currently set to deploy any ' + 'profile, network boot attempts will be ignored') + candmgrs = cfd.get(args.node, {}).get( + 'collective.managercandidates', {}).get('value', None) + if candmgrs: + try: + candmgrs = noderange.NodeRange(candmgrs, cfg).nodes + except Exception: # fallback to unverified noderange + candmgrs = noderange.NodeRange(candmgrs).nodes + if collective.get_myname() not in candmgrs: + emprint(f'{args.node} has deployment restricted to ' + 'certain collective managers excluding the ' + 'system running the selfcheck') + print(f"Checking network configuration for {args.node}") bootablev4nics = [] bootablev6nics = [] targsships = [] From c3e918fc5fa3dbc7c9bf6d4b247b657b72279c05 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 25 Jul 2024 09:42:24 -0400 Subject: [PATCH 053/146] Fix mistake in untethered support --- .../ubuntu20.04-diskless/profiles/default/scripts/imageboot.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_osdeploy/ubuntu20.04-diskless/profiles/default/scripts/imageboot.sh b/confluent_osdeploy/ubuntu20.04-diskless/profiles/default/scripts/imageboot.sh index ff8d253d..0db99754 100644 --- a/confluent_osdeploy/ubuntu20.04-diskless/profiles/default/scripts/imageboot.sh +++ b/confluent_osdeploy/ubuntu20.04-diskless/profiles/default/scripts/imageboot.sh @@ -8,7 +8,7 @@ for addr in $(grep ^MANAGER: /etc/confluent/confluent.info|awk '{print $2}'|sed fi done mkdir -p /mnt/remoteimg /mnt/remote /mnt/overlay -if grep confluennt_imagemethod=untethered /proc/cmdline > /dev/null; then +if grep confluent_imagemethod=untethered /proc/cmdline > /dev/null; then mount -t tmpfs untethered /mnt/remoteimg curl https://$confluent_mgr/confluent-public/os/$confluent_profile/rootimg.sfs -o /mnt/remoteimg/rootimg.sfs else From 0f955cd068ca67182d668a693398bdace02d4b9b Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 25 Jul 2024 11:24:41 -0400 Subject: [PATCH 054/146] Begin work on a cryptboot support for ubuntu Start implementing a tpm2-initramfs-tool based approach. This requires a bit of an odd transition as the PCR 7 is likely to change between the install phase and the boot phase, so we have to select different PCRs, but that requires an argument to pass that crypttab does not support. --- .../profiles/default/autoinstall/user-data | 1 + .../profiles/default/scripts/post.sh | 26 ++++++++++++++++++- .../profiles/default/scripts/pre.sh | 12 +++++---- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/confluent_osdeploy/ubuntu22.04/profiles/default/autoinstall/user-data b/confluent_osdeploy/ubuntu22.04/profiles/default/autoinstall/user-data index 5b6c9894..7c4181d4 100644 --- a/confluent_osdeploy/ubuntu22.04/profiles/default/autoinstall/user-data +++ b/confluent_osdeploy/ubuntu22.04/profiles/default/autoinstall/user-data @@ -10,6 +10,7 @@ autoinstall: storage: layout: name: lvm +#CRYPTBOOT password: %%CRYPTPASS%% match: path: "%%INSTALLDISK%%" user-data: diff --git a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/post.sh b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/post.sh index d9730889..69e1593e 100755 --- a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/post.sh +++ b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/post.sh @@ -60,10 +60,12 @@ cp /custom-installation/confluent/bin/apiclient /target/opt/confluent/bin mount -o bind /dev /target/dev mount -o bind /proc /target/proc mount -o bind /sys /target/sys +mount -o bind /run /target/run mount -o bind /sys/firmware/efi/efivars /target/sys/firmware/efi/efivars if [ 1 = $updategrub ]; then chroot /target update-grub fi + echo "Port 22" >> /etc/ssh/sshd_config echo "Port 2222" >> /etc/ssh/sshd_config echo "Match LocalPort 22" >> /etc/ssh/sshd_config @@ -88,8 +90,30 @@ chroot /target bash -c "source /etc/confluent/functions; run_remote_parts post.d source /target/etc/confluent/functions run_remote_config post + +if [ -f /etc/confluent_lukspass ]; then + $lukspass=$(cat /etc/confluent_lukspass) + chroot /target apt install tpm2-initramfs-tool + chroot /target tpm2-initramfs-tool seal --data "$(lukspass)" > /dev/null + # The default PCR 7 mutates, and crypttab does not provide a way to pass args + cat > /target/usr/bin/tpm2-initramfs-tool.pcr0 << EOF +#!/bin/sh +tpm2-initramfs-tool -p 0 \$* +EOF + chmod 755 /target/usr/bin/tpm2-initramfs-tool.pcr0 + cat > /target/etc/initramfs-tools/hooks/tpm2-initramfs-tool < /dev/console diff --git a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh index 5db222a7..ee61ac26 100755 --- a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh +++ b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh @@ -13,11 +13,6 @@ exec 2>> /var/log/confluent/confluent-pre.log chmod 600 /var/log/confluent/confluent-pre.log cryptboot=$(grep encryptboot: $deploycfg|sed -e 's/^encryptboot: //') -if [ "$cryptboot" != "" ] && [ "$cryptboot" != "none" ] && [ "$cryptboot" != "null" ]; then - echo "****Encrypted boot requested, but not implemented for this OS, halting install" > /dev/console - [ -f '/tmp/autoconsdev' ] && (echo "****Encryptod boot requested, but not implemented for this OS,halting install" >> $(cat /tmp/autoconsdev)) - while :; do sleep 86400; done -fi cat /custom-installation/ssh/*pubkey > /root/.ssh/authorized_keys @@ -45,6 +40,13 @@ if [ ! -e /tmp/installdisk ]; then python3 /custom-installation/getinstalldisk fi sed -i s!%%INSTALLDISK%%!/dev/$(cat /tmp/installdisk)! /autoinstall.yaml +if [ "$cryptboot" != "" ] && [ "$cryptboot" != "none" ] && [ "$cryptboot" != "null" ]; then + lukspass=$(head -c 64 < /dev/urandom |base64) + sed -i s!%%CRYPTPASS%%!$lukspass! /autoinstall.yaml + sed -i s!'#CRYPTBOOT'!! /autoinstall.yaml + echo $lukspass > /etc/confluent_lukspass + +fi ) & tail --pid $! -n 0 -F /var/log/confluent/confluent-pre.log > /dev/console From 41b722c3f7d583381008fb86968390033225cba1 Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Thu, 25 Jul 2024 18:38:23 +0200 Subject: [PATCH 055/146] Use natural sort for lists in json dumps Previously, items were randomly arranged in lists in the json dump. This meant that the JSON files were different after each export. Now they are naturally sorted and identical. This should make it easier to save and compare the JSON dumps in version control systems. --- confluent_server/confluent/config/configmanager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_server/confluent/config/configmanager.py b/confluent_server/confluent/config/configmanager.py index 528924e8..6cbf4604 100644 --- a/confluent_server/confluent/config/configmanager.py +++ b/confluent_server/confluent/config/configmanager.py @@ -2647,7 +2647,7 @@ class ConfigManager(object): dumpdata[confarea][element][attribute]['cryptvalue'] = '!'.join(cryptval) elif isinstance(dumpdata[confarea][element][attribute], set): dumpdata[confarea][element][attribute] = \ - list(dumpdata[confarea][element][attribute]) + confluent.util.natural_sort(list(dumpdata[confarea][element][attribute])) return json.dumps( dumpdata, sort_keys=True, indent=4, separators=(',', ': ')) From 80296b6cbc8c9c2b03b25f03078fce0e2d66d5c6 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 25 Jul 2024 14:05:10 -0400 Subject: [PATCH 056/146] Point to the C context object rather than python class The OpenSSL variant of Context is a python class, but it does have a C context in it. --- confluent_server/confluent/sockapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_server/confluent/sockapi.py b/confluent_server/confluent/sockapi.py index 8aca0058..86534767 100644 --- a/confluent_server/confluent/sockapi.py +++ b/confluent_server/confluent/sockapi.py @@ -431,7 +431,7 @@ def _tlsstartup(cnn): ctx.use_certificate_file('/etc/confluent/srvcert.pem') ctx.use_privatekey_file('/etc/confluent/privkey.pem') ctx.set_verify(libssln.VERIFY_PEER, lambda *args: True) - ssl_ctx = PySSLContext.from_address(id(ctx)).ctx + ssl_ctx = PySSLContext.from_address(id(ctx._context)).ctx libsslc.SSL_CTX_set_cert_verify_callback(ssl_ctx, verify_stub, 0) cnn = libssl.Connection(ctx, cnn) cnn.set_accept_state() From 298be3b30a385af3c2506ba2737dbb530ac38e1d Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 25 Jul 2024 14:05:10 -0400 Subject: [PATCH 057/146] Point to the C context object rather than python class The OpenSSL variant of Context is a python class, but it does have a C context in it. --- confluent_server/confluent/sockapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_server/confluent/sockapi.py b/confluent_server/confluent/sockapi.py index 8aca0058..86534767 100644 --- a/confluent_server/confluent/sockapi.py +++ b/confluent_server/confluent/sockapi.py @@ -431,7 +431,7 @@ def _tlsstartup(cnn): ctx.use_certificate_file('/etc/confluent/srvcert.pem') ctx.use_privatekey_file('/etc/confluent/privkey.pem') ctx.set_verify(libssln.VERIFY_PEER, lambda *args: True) - ssl_ctx = PySSLContext.from_address(id(ctx)).ctx + ssl_ctx = PySSLContext.from_address(id(ctx._context)).ctx libsslc.SSL_CTX_set_cert_verify_callback(ssl_ctx, verify_stub, 0) cnn = libssl.Connection(ctx, cnn) cnn.set_accept_state() From 30aa6f382c47c0a5b04269f96daa20690ae296b3 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 25 Jul 2024 14:54:15 -0400 Subject: [PATCH 058/146] Ignore duplicate specifications of same key Particularly if traversing a lot of linked configuration, the same key/cert path may come up multiple times, check for equality and if equal, just keep going. --- confluent_server/confluent/certutil.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_server/confluent/certutil.py b/confluent_server/confluent/certutil.py index 9a478787..4ac67165 100644 --- a/confluent_server/confluent/certutil.py +++ b/confluent_server/confluent/certutil.py @@ -76,7 +76,7 @@ def get_certificate_paths(): continue kploc = check_apache_config(os.path.join(currpath, fname)) - if keypath and kploc[0]: + if keypath and kploc[0] and keypath != kploc[0]: return None, None # Ambiguous... if kploc[0]: keypath, certpath = kploc From 626f16cb6fcac5a7c9531014766b287ac9ca2d72 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 25 Jul 2024 14:54:15 -0400 Subject: [PATCH 059/146] Ignore duplicate specifications of same key Particularly if traversing a lot of linked configuration, the same key/cert path may come up multiple times, check for equality and if equal, just keep going. --- confluent_server/confluent/certutil.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_server/confluent/certutil.py b/confluent_server/confluent/certutil.py index 9a478787..4ac67165 100644 --- a/confluent_server/confluent/certutil.py +++ b/confluent_server/confluent/certutil.py @@ -76,7 +76,7 @@ def get_certificate_paths(): continue kploc = check_apache_config(os.path.join(currpath, fname)) - if keypath and kploc[0]: + if keypath and kploc[0] and keypath != kploc[0]: return None, None # Ambiguous... if kploc[0]: keypath, certpath = kploc From 956e473fa6ea6fe3af1c61d32f02090ab6b59857 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 25 Jul 2024 15:25:09 -0400 Subject: [PATCH 060/146] Have SSDP fallback to unverified noderanges when looking at candidates --- confluent_server/confluent/discovery/protocols/ssdp.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/confluent_server/confluent/discovery/protocols/ssdp.py b/confluent_server/confluent/discovery/protocols/ssdp.py index 3c1edc74..12ec4ba7 100644 --- a/confluent_server/confluent/discovery/protocols/ssdp.py +++ b/confluent_server/confluent/discovery/protocols/ssdp.py @@ -251,7 +251,10 @@ def snoop(handler, byehandler=None, protocol=None, uuidlookup=None): break candmgrs = cfd.get(node, {}).get('collective.managercandidates', {}).get('value', None) if candmgrs: - candmgrs = noderange.NodeRange(candmgrs, cfg).nodes + try: + candmgrs = noderange.NodeRange(candmgrs, cfg).nodes + except Exception: + candmgrs = noderange.NodeRange(candmgrs).nodes if collective.get_myname() not in candmgrs: break currtime = time.time() From dc7c9f4a3d324c8881fd312d8132ed8207f64e15 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 25 Jul 2024 15:25:09 -0400 Subject: [PATCH 061/146] Have SSDP fallback to unverified noderanges when looking at candidates --- confluent_server/confluent/discovery/protocols/ssdp.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/confluent_server/confluent/discovery/protocols/ssdp.py b/confluent_server/confluent/discovery/protocols/ssdp.py index 3c1edc74..12ec4ba7 100644 --- a/confluent_server/confluent/discovery/protocols/ssdp.py +++ b/confluent_server/confluent/discovery/protocols/ssdp.py @@ -251,7 +251,10 @@ def snoop(handler, byehandler=None, protocol=None, uuidlookup=None): break candmgrs = cfd.get(node, {}).get('collective.managercandidates', {}).get('value', None) if candmgrs: - candmgrs = noderange.NodeRange(candmgrs, cfg).nodes + try: + candmgrs = noderange.NodeRange(candmgrs, cfg).nodes + except Exception: + candmgrs = noderange.NodeRange(candmgrs).nodes if collective.get_myname() not in candmgrs: break currtime = time.time() From 1d6009a2f2d58211e031e8290d3367a2937422bb Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 26 Jul 2024 10:33:38 -0400 Subject: [PATCH 062/146] Switch to using systemd-cryptenroll The design more cleanly uses luks slot, but requires providing initramfs hooks. Those hooks are provided now. --- .../profiles/default/scripts/post.sh | 73 +++++++++++++++---- .../profiles/default/scripts/pre.sh | 9 ++- 2 files changed, 65 insertions(+), 17 deletions(-) diff --git a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/post.sh b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/post.sh index 69e1593e..2c8be0c0 100755 --- a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/post.sh +++ b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/post.sh @@ -92,23 +92,66 @@ source /target/etc/confluent/functions run_remote_config post if [ -f /etc/confluent_lukspass ]; then - $lukspass=$(cat /etc/confluent_lukspass) - chroot /target apt install tpm2-initramfs-tool - chroot /target tpm2-initramfs-tool seal --data "$(lukspass)" > /dev/null - # The default PCR 7 mutates, and crypttab does not provide a way to pass args - cat > /target/usr/bin/tpm2-initramfs-tool.pcr0 << EOF -#!/bin/sh -tpm2-initramfs-tool -p 0 \$* -EOF - chmod 755 /target/usr/bin/tpm2-initramfs-tool.pcr0 - cat > /target/etc/initramfs-tools/hooks/tpm2-initramfs-tool </target/etc/initramfs-tools/scripts/local-top/systemdecrypt << EOS +#!/bin/sh +case \$1 in +prereqs) + echo + exit 0 + ;; +esac + +systemdecryptnow() { +. /usr/lib/cryptsetup/functions +local CRYPTTAB_SOURCE=\$(awk '{print \$2}' /systemdecrypt/crypttab) +local CRYPTTAB_NAME=\$(awk '{print \$1}' /systemdecrypt/crypttab) +crypttab_resolve_source +/lib/systemd/systemd-cryptsetup attach "\${CRYPTTAB_NAME}" "\${CRYPTTAB_SOURCE}" none tpm2-device=auto +} + +systemdecryptnow +EOS + chmod 755 /target/etc/initramfs-tools/scripts/local-top/systemdecrypt + cat > /target/etc/initramfs-tools/hooks/systemdecrypt <> \$DESTDIR/scripts/local-top/ORDER + +if [ -f \$DESTDIR/cryptroot/crypttab ]; then + mv \$DESTDIR/cryptroot/crypttab \$DESTDIR/systemdecrypt/crypttab +fi EOF - chmod 755 /target/etc/initramfs-tools/hooks/tpm2-initramfs-tool chroot /target update-initramfs -u fi python3 /opt/confluent/bin/apiclient /confluent-api/self/updatestatus -d 'status: staged' diff --git a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh index ee61ac26..bfe1c7db 100755 --- a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh +++ b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh @@ -41,10 +41,15 @@ if [ ! -e /tmp/installdisk ]; then fi sed -i s!%%INSTALLDISK%%!/dev/$(cat /tmp/installdisk)! /autoinstall.yaml if [ "$cryptboot" != "" ] && [ "$cryptboot" != "none" ] && [ "$cryptboot" != "null" ]; then - lukspass=$(head -c 64 < /dev/urandom |base64) + if ! grep '#CRYPTBOOT' /autoinstall.yaml > /dev/null; then + echo "****Encrypted boot requested, but the user-data does not have a hook to enable,halting install" > /dev/console + [ -f '/tmp/autoconsdev' ] && (echo "****Encryptod boot requested, but the user-data does not have a hook to enable,halting install" >> $(cat /tmp/autoconsdev)) + while :; do sleep 86400; done + fi + lukspass=$(head -c 66 < /dev/urandom |base64 -w0) sed -i s!%%CRYPTPASS%%!$lukspass! /autoinstall.yaml sed -i s!'#CRYPTBOOT'!! /autoinstall.yaml - echo $lukspass > /etc/confluent_lukspass + echo -n $lukspass > /etc/confluent_lukspass fi ) & From 58ee85f39ebe558f11108942f18c8966e2cae896 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 26 Jul 2024 11:33:01 -0400 Subject: [PATCH 063/146] Rework Ubuntu addcrypt support The comment based hook is destroyed during early install process. Use python to manipulate the autoinstall file in a more sophisticated way. Also refactor the initramfs hook material to be standalone files. --- .../profiles/default/autoinstall/user-data | 1 - .../profiles/default/scripts/addcrypt | 12 +++++ .../profiles/default/scripts/post.sh | 49 ++----------------- .../profiles/default/scripts/pre.sh | 3 +- .../profiles/default/scripts/systemdecrypt | 17 +++++++ .../default/scripts/systemdecrypt-hook | 22 +++++++++ 6 files changed, 58 insertions(+), 46 deletions(-) create mode 100644 confluent_osdeploy/ubuntu22.04/profiles/default/scripts/addcrypt create mode 100644 confluent_osdeploy/ubuntu22.04/profiles/default/scripts/systemdecrypt create mode 100644 confluent_osdeploy/ubuntu22.04/profiles/default/scripts/systemdecrypt-hook diff --git a/confluent_osdeploy/ubuntu22.04/profiles/default/autoinstall/user-data b/confluent_osdeploy/ubuntu22.04/profiles/default/autoinstall/user-data index 7c4181d4..5b6c9894 100644 --- a/confluent_osdeploy/ubuntu22.04/profiles/default/autoinstall/user-data +++ b/confluent_osdeploy/ubuntu22.04/profiles/default/autoinstall/user-data @@ -10,7 +10,6 @@ autoinstall: storage: layout: name: lvm -#CRYPTBOOT password: %%CRYPTPASS%% match: path: "%%INSTALLDISK%%" user-data: diff --git a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/addcrypt b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/addcrypt new file mode 100644 index 00000000..4f2ae905 --- /dev/null +++ b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/addcrypt @@ -0,0 +1,12 @@ +import yaml +import sys + +ainst = {} +with open('/autoinstall.yaml', 'r') as allin: + ainst = yaml.safe_load(allin) + +ainst['storage']['layout']['password'] = sys.argv[1] + +with open('/autoinstall.yaml', 'w') as allout: + yaml.safe_dump(ainst, allout) + diff --git a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/post.sh b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/post.sh index 2c8be0c0..998f7bda 100755 --- a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/post.sh +++ b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/post.sh @@ -108,50 +108,11 @@ if [ -f /etc/confluent_lukspass ]; then $lukspass=$(cat /etc/confluent_lukspass) chroot /target apt install libtss2-rc0 PASSWORD=$(lukspass) chroot /target systemd-cryptenroll --tpm2-device=auto $CRYPTTAB_SOURCE - cat >/target/etc/initramfs-tools/scripts/local-top/systemdecrypt << EOS -#!/bin/sh -case \$1 in -prereqs) - echo - exit 0 - ;; -esac - -systemdecryptnow() { -. /usr/lib/cryptsetup/functions -local CRYPTTAB_SOURCE=\$(awk '{print \$2}' /systemdecrypt/crypttab) -local CRYPTTAB_NAME=\$(awk '{print \$1}' /systemdecrypt/crypttab) -crypttab_resolve_source -/lib/systemd/systemd-cryptsetup attach "\${CRYPTTAB_NAME}" "\${CRYPTTAB_SOURCE}" none tpm2-device=auto -} - -systemdecryptnow -EOS - chmod 755 /target/etc/initramfs-tools/scripts/local-top/systemdecrypt - cat > /target/etc/initramfs-tools/hooks/systemdecrypt <> \$DESTDIR/scripts/local-top/ORDER - -if [ -f \$DESTDIR/cryptroot/crypttab ]; then - mv \$DESTDIR/cryptroot/crypttab \$DESTDIR/systemdecrypt/crypttab -fi -EOF + fetch_remote systemdecrypt + mv systemdecrypt /target/etc/initramfs-tools/scripts/local-top/systemdecrypt + fetch_remote systemdecrypt-hook + mv systemdecrypt-hook /target/etc/initramfs-tools/hooks/systemdecrypt + chmod 755 /target/etc/initramfs-tools/scripts/local-top/systemdecrypt /target/etc/initramfs-tools/hooks/systemdecrypt chroot /target update-initramfs -u fi python3 /opt/confluent/bin/apiclient /confluent-api/self/updatestatus -d 'status: staged' diff --git a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh index bfe1c7db..db0e967d 100755 --- a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh +++ b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh @@ -41,12 +41,13 @@ if [ ! -e /tmp/installdisk ]; then fi sed -i s!%%INSTALLDISK%%!/dev/$(cat /tmp/installdisk)! /autoinstall.yaml if [ "$cryptboot" != "" ] && [ "$cryptboot" != "none" ] && [ "$cryptboot" != "null" ]; then + lukspass=$(head -c 66 < /dev/urandom |base64 -w0) + run_remote_python addcrypt if ! grep '#CRYPTBOOT' /autoinstall.yaml > /dev/null; then echo "****Encrypted boot requested, but the user-data does not have a hook to enable,halting install" > /dev/console [ -f '/tmp/autoconsdev' ] && (echo "****Encryptod boot requested, but the user-data does not have a hook to enable,halting install" >> $(cat /tmp/autoconsdev)) while :; do sleep 86400; done fi - lukspass=$(head -c 66 < /dev/urandom |base64 -w0) sed -i s!%%CRYPTPASS%%!$lukspass! /autoinstall.yaml sed -i s!'#CRYPTBOOT'!! /autoinstall.yaml echo -n $lukspass > /etc/confluent_lukspass diff --git a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/systemdecrypt b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/systemdecrypt new file mode 100644 index 00000000..6f0cbaed --- /dev/null +++ b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/systemdecrypt @@ -0,0 +1,17 @@ +#!/bin/sh +case $1 in +prereqs) + echo + exit 0 + ;; +esac + +systemdecryptnow() { +. /usr/lib/cryptsetup/functions +local CRYPTTAB_SOURCE=$(awk '{print $2}' /systemdecrypt/crypttab) +local CRYPTTAB_NAME=$(awk '{print $1}' /systemdecrypt/crypttab) +crypttab_resolve_source +/lib/systemd/systemd-cryptsetup attach "${CRYPTTAB_NAME}" "${CRYPTTAB_SOURCE}" none tpm2-device=auto +} + +systemdecryptnow diff --git a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/systemdecrypt-hook b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/systemdecrypt-hook new file mode 100644 index 00000000..48c9d16d --- /dev/null +++ b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/systemdecrypt-hook @@ -0,0 +1,22 @@ +#!/bin/sh +case "$1" in + prereqs) + echo + exit 0 + ;; +esac + +. /usr/share/initramfs-tools/hook-functions +mkdir -p $DESTDIR/systemdecrypt +copy_exec /lib/systemd/systemd-cryptsetup /lib/systemd +for i in /lib/x86_64-linux-gnu/libtss2* +do + copy_exec ${i} /lib/x86_64-linux-gnu +done +mkdir -p $DESTDIR/scripts/local-top + +echo /scripts/local-top/systemdecrypt >> $DESTDIR/scripts/local-top/ORDER + +if [ -f $DESTDIR/cryptroot/crypttab ]; then + mv $DESTDIR/cryptroot/crypttab $DESTDIR/systemdecrypt/crypttab +fi From f482d2ead993ae651f742e43b5977406e5d64cb4 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 26 Jul 2024 11:35:49 -0400 Subject: [PATCH 064/146] Amend crypt hook check The comment was changed, check for password instead. --- confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh index db0e967d..02402a99 100755 --- a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh +++ b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh @@ -43,7 +43,7 @@ sed -i s!%%INSTALLDISK%%!/dev/$(cat /tmp/installdisk)! /autoinstall.yaml if [ "$cryptboot" != "" ] && [ "$cryptboot" != "none" ] && [ "$cryptboot" != "null" ]; then lukspass=$(head -c 66 < /dev/urandom |base64 -w0) run_remote_python addcrypt - if ! grep '#CRYPTBOOT' /autoinstall.yaml > /dev/null; then + if ! grep 'pasword:' /autoinstall.yaml > /dev/null; then echo "****Encrypted boot requested, but the user-data does not have a hook to enable,halting install" > /dev/console [ -f '/tmp/autoconsdev' ] && (echo "****Encryptod boot requested, but the user-data does not have a hook to enable,halting install" >> $(cat /tmp/autoconsdev)) while :; do sleep 86400; done From 1ddf735590bd6479dc7fa797a88869adaa963283 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 26 Jul 2024 11:50:53 -0400 Subject: [PATCH 065/146] Fix omitted argument to addcrypt --- confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh index 02402a99..bd9d1d60 100755 --- a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh +++ b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh @@ -42,7 +42,7 @@ fi sed -i s!%%INSTALLDISK%%!/dev/$(cat /tmp/installdisk)! /autoinstall.yaml if [ "$cryptboot" != "" ] && [ "$cryptboot" != "none" ] && [ "$cryptboot" != "null" ]; then lukspass=$(head -c 66 < /dev/urandom |base64 -w0) - run_remote_python addcrypt + run_remote_python addcrypt "$lukspass" if ! grep 'pasword:' /autoinstall.yaml > /dev/null; then echo "****Encrypted boot requested, but the user-data does not have a hook to enable,halting install" > /dev/console [ -f '/tmp/autoconsdev' ] && (echo "****Encryptod boot requested, but the user-data does not have a hook to enable,halting install" >> $(cat /tmp/autoconsdev)) From c1747ad24ca961b10df0e94819b58861f7b2f0bb Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 26 Jul 2024 11:54:10 -0400 Subject: [PATCH 066/146] Correct spelling of key for luks check --- confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh index bd9d1d60..77a16906 100755 --- a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh +++ b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh @@ -43,7 +43,7 @@ sed -i s!%%INSTALLDISK%%!/dev/$(cat /tmp/installdisk)! /autoinstall.yaml if [ "$cryptboot" != "" ] && [ "$cryptboot" != "none" ] && [ "$cryptboot" != "null" ]; then lukspass=$(head -c 66 < /dev/urandom |base64 -w0) run_remote_python addcrypt "$lukspass" - if ! grep 'pasword:' /autoinstall.yaml > /dev/null; then + if ! grep 'password:' /autoinstall.yaml > /dev/null; then echo "****Encrypted boot requested, but the user-data does not have a hook to enable,halting install" > /dev/console [ -f '/tmp/autoconsdev' ] && (echo "****Encryptod boot requested, but the user-data does not have a hook to enable,halting install" >> $(cat /tmp/autoconsdev)) while :; do sleep 86400; done From c563f48c71acfd9ffc44c1f88caef339b527de9d Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 26 Jul 2024 12:30:41 -0400 Subject: [PATCH 067/146] Fix assignment of lukspass variable. --- confluent_osdeploy/ubuntu22.04/profiles/default/scripts/post.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/post.sh b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/post.sh index 998f7bda..28d45e41 100755 --- a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/post.sh +++ b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/post.sh @@ -105,7 +105,7 @@ if [ -f /etc/confluent_lukspass ]; then wall "Unable to find $CRYPTTAB_SOURCE, halting install" while :; do sleep 86400; done fi - $lukspass=$(cat /etc/confluent_lukspass) + lukspass=$(cat /etc/confluent_lukspass) chroot /target apt install libtss2-rc0 PASSWORD=$(lukspass) chroot /target systemd-cryptenroll --tpm2-device=auto $CRYPTTAB_SOURCE fetch_remote systemdecrypt From 7a602f58b2e1b62a0c18b4f5e9b72d20817bbf5d Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 26 Jul 2024 13:47:13 -0400 Subject: [PATCH 068/146] Fixes for ubuntu profile tpm support --- confluent_osdeploy/ubuntu22.04/profiles/default/scripts/post.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/post.sh b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/post.sh index 28d45e41..4af3a01f 100755 --- a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/post.sh +++ b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/post.sh @@ -107,7 +107,7 @@ if [ -f /etc/confluent_lukspass ]; then fi lukspass=$(cat /etc/confluent_lukspass) chroot /target apt install libtss2-rc0 - PASSWORD=$(lukspass) chroot /target systemd-cryptenroll --tpm2-device=auto $CRYPTTAB_SOURCE + PASSWORD=$lukspass chroot /target systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs="" $CRYPTTAB_SOURCE fetch_remote systemdecrypt mv systemdecrypt /target/etc/initramfs-tools/scripts/local-top/systemdecrypt fetch_remote systemdecrypt-hook From 2df902e80e2df7782f6c594c3195862d1c04ea69 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 26 Jul 2024 14:07:54 -0400 Subject: [PATCH 069/146] Remove luks password from argv Pass the luks password by environment variable instead. --- .../ubuntu22.04/profiles/default/scripts/addcrypt | 4 ++-- .../ubuntu22.04/profiles/default/scripts/pre.sh | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/addcrypt b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/addcrypt index 4f2ae905..750753c1 100644 --- a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/addcrypt +++ b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/addcrypt @@ -1,11 +1,11 @@ import yaml -import sys +import os ainst = {} with open('/autoinstall.yaml', 'r') as allin: ainst = yaml.safe_load(allin) -ainst['storage']['layout']['password'] = sys.argv[1] +ainst['storage']['layout']['password'] = os.environ['lukspass'] with open('/autoinstall.yaml', 'w') as allout: yaml.safe_dump(ainst, allout) diff --git a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh index 77a16906..4ec3f822 100755 --- a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh +++ b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh @@ -42,7 +42,8 @@ fi sed -i s!%%INSTALLDISK%%!/dev/$(cat /tmp/installdisk)! /autoinstall.yaml if [ "$cryptboot" != "" ] && [ "$cryptboot" != "none" ] && [ "$cryptboot" != "null" ]; then lukspass=$(head -c 66 < /dev/urandom |base64 -w0) - run_remote_python addcrypt "$lukspass" + export lukspass + run_remote_python addcrypt if ! grep 'password:' /autoinstall.yaml > /dev/null; then echo "****Encrypted boot requested, but the user-data does not have a hook to enable,halting install" > /dev/console [ -f '/tmp/autoconsdev' ] && (echo "****Encryptod boot requested, but the user-data does not have a hook to enable,halting install" >> $(cat /tmp/autoconsdev)) From 332068074d1b93d1b4b9a85e38643ba0d93f85ba Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 26 Jul 2024 16:54:58 -0400 Subject: [PATCH 070/146] Extend systemdecrypt hook to support Ubuntu 24.04 Ubuntu 240.4 systemd-cryptsetup now has an external dependency. --- .../ubuntu22.04/profiles/default/scripts/systemdecrypt-hook | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/systemdecrypt-hook b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/systemdecrypt-hook index 48c9d16d..ee602c7c 100644 --- a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/systemdecrypt-hook +++ b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/systemdecrypt-hook @@ -13,6 +13,10 @@ for i in /lib/x86_64-linux-gnu/libtss2* do copy_exec ${i} /lib/x86_64-linux-gnu done +if [ -f /lib/x86_64-linux-gnu/cryptsetup/libcryptsetup-token-systemd-tpm2.so ]; then + mkdir -p $DESTDIR/lib/x86_64-linux-gnu/cryptsetup + copy_exec /lib/x86_64-linux-gnu/cryptsetup/libcryptsetup-token-systemd-tpm2.so /lib/x86_64-linux-gnu/cryptsetup +fi mkdir -p $DESTDIR/scripts/local-top echo /scripts/local-top/systemdecrypt >> $DESTDIR/scripts/local-top/ORDER From 1af898dcb8a07ccf537bf41cb592066ce45fc0e2 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 26 Jul 2024 17:43:51 -0400 Subject: [PATCH 071/146] Fix encryptboot on EL8/EL9 --- confluent_osdeploy/el8/profiles/default/scripts/pre.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_osdeploy/el8/profiles/default/scripts/pre.sh b/confluent_osdeploy/el8/profiles/default/scripts/pre.sh index cd831360..89d989b8 100644 --- a/confluent_osdeploy/el8/profiles/default/scripts/pre.sh +++ b/confluent_osdeploy/el8/profiles/default/scripts/pre.sh @@ -114,7 +114,7 @@ confluentpython /etc/confluent/apiclient /confluent-public/os/$confluent_profile grep '^%include /tmp/partitioning' /tmp/kickstart.* > /dev/null || rm /tmp/installdisk if [ -e /tmp/installdisk -a ! -e /tmp/partitioning ]; then INSTALLDISK=$(cat /tmp/installdisk) - sed -e s/%%INSTALLDISK%%/$INSTALLDISK/ -e s/%%LUKSHOOK%%/$LUKSPARTY/ /tmp/partitioning.template > /tmp/partitioning + sed -e s/%%INSTALLDISK%%/$INSTALLDISK/ -e "s/%%LUKSHOOK%%/$LUKSPARTY/" /tmp/partitioning.template > /tmp/partitioning vgchange -a n >& /dev/null wipefs -a -f /dev/$INSTALLDISK >& /dev/null fi From bee9f1819717b2c5475671d9d77293f1bf11ca47 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 26 Jul 2024 17:59:42 -0400 Subject: [PATCH 072/146] Tolerate / in the apikey for LUKS setup The apikey is highly likely to have a /, and so we need to use something not in the base64 alphabet as a delimiter. --- confluent_osdeploy/el8/profiles/default/scripts/pre.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_osdeploy/el8/profiles/default/scripts/pre.sh b/confluent_osdeploy/el8/profiles/default/scripts/pre.sh index 89d989b8..4deff814 100644 --- a/confluent_osdeploy/el8/profiles/default/scripts/pre.sh +++ b/confluent_osdeploy/el8/profiles/default/scripts/pre.sh @@ -114,7 +114,7 @@ confluentpython /etc/confluent/apiclient /confluent-public/os/$confluent_profile grep '^%include /tmp/partitioning' /tmp/kickstart.* > /dev/null || rm /tmp/installdisk if [ -e /tmp/installdisk -a ! -e /tmp/partitioning ]; then INSTALLDISK=$(cat /tmp/installdisk) - sed -e s/%%INSTALLDISK%%/$INSTALLDISK/ -e "s/%%LUKSHOOK%%/$LUKSPARTY/" /tmp/partitioning.template > /tmp/partitioning + sed -e s/%%INSTALLDISK%%/$INSTALLDISK/ -e "s!%%LUKSHOOK%%!$LUKSPARTY!" /tmp/partitioning.template > /tmp/partitioning vgchange -a n >& /dev/null wipefs -a -f /dev/$INSTALLDISK >& /dev/null fi From 329f2b4485fc26005525bd11616cc95d1641e0d3 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 29 Jul 2024 10:17:14 -0400 Subject: [PATCH 073/146] Amend cryptboot implementation for Ubuntu 22/24, EL8/EL9 Provide mechanism for administrator to place a custom key for potential interactive recovery into /var/lib/confluent/private/os//pending/luks.key If not provided, generate a unique one for each install. Either way, persist the key in /etc/confluent/luks.key, to facilitate later resealing if the user wants (clevis nor systemd prior to 256 supports unlock via TPM2, so keyfile is required for now). Migrating to otherwise escrowed passphrases and/or sealing to specific TPMs will be left to operators and/or third parties. --- confluent_osdeploy/el8/profiles/default/scripts/pre.sh | 10 ++++++++-- .../el8/profiles/default/scripts/tpm_luks.sh | 5 +++-- .../ubuntu22.04/profiles/default/scripts/post.sh | 2 ++ .../ubuntu22.04/profiles/default/scripts/pre.sh | 7 +++++-- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/confluent_osdeploy/el8/profiles/default/scripts/pre.sh b/confluent_osdeploy/el8/profiles/default/scripts/pre.sh index 4deff814..880d22ac 100644 --- a/confluent_osdeploy/el8/profiles/default/scripts/pre.sh +++ b/confluent_osdeploy/el8/profiles/default/scripts/pre.sh @@ -90,8 +90,14 @@ touch /tmp/cryptpkglist touch /tmp/pkglist touch /tmp/addonpackages if [ "$cryptboot" == "tpm2" ]; then - LUKSPARTY="--encrypted --passphrase=$(cat /etc/confluent/confluent.apikey)" - echo $cryptboot >> /tmp/cryptboot + lukspass=$(python3 /opt/confluent/bin/apiclient /confluent-api/self/profileprivate/pending/luks.key 2> /dev/null) + if [ -z "$lukspass" ]; then + lukspass=$(python3 -c 'import os;import base64;print(base64.b64encode(os.urandom(66)).decode())') + fi + echo $lukspass > /etc/confluent/luks.key + chmod 000 /etc/confluent/luks.key + LUKSPARTY="--encrypted --passphrase=$lukspass" + echo $cryptboot >> /tmp/cryptboot echo clevis-dracut >> /tmp/cryptpkglist fi diff --git a/confluent_osdeploy/el8/profiles/default/scripts/tpm_luks.sh b/confluent_osdeploy/el8/profiles/default/scripts/tpm_luks.sh index df9c857f..c457ffd4 100644 --- a/confluent_osdeploy/el8/profiles/default/scripts/tpm_luks.sh +++ b/confluent_osdeploy/el8/profiles/default/scripts/tpm_luks.sh @@ -1,4 +1,5 @@ #!/bin/sh cryptdisk=$(blkid -t TYPE="crypto_LUKS"|sed -e s/:.*//) -clevis luks bind -f -d $cryptdisk -k - tpm2 '{}' < /etc/confluent/confluent.apikey -cryptsetup luksRemoveKey $cryptdisk < /etc/confluent/confluent.apikey +clevis luks bind -f -d $cryptdisk -k /etc/cofluent/luks.key tpm2 '{}' +chmod 000 /etc/confluent/luks.key +#cryptsetup luksRemoveKey $cryptdisk < /etc/confluent/confluent.apikey diff --git a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/post.sh b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/post.sh index 4af3a01f..a86695ca 100755 --- a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/post.sh +++ b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/post.sh @@ -105,6 +105,8 @@ if [ -f /etc/confluent_lukspass ]; then wall "Unable to find $CRYPTTAB_SOURCE, halting install" while :; do sleep 86400; done fi + cp /etc/confluent_lukspass /target/etc/confluent/luks.key + chmod 000 /target/etc/confluent/luks.key lukspass=$(cat /etc/confluent_lukspass) chroot /target apt install libtss2-rc0 PASSWORD=$lukspass chroot /target systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs="" $CRYPTTAB_SOURCE diff --git a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh index 4ec3f822..5b609565 100755 --- a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh +++ b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh @@ -41,7 +41,10 @@ if [ ! -e /tmp/installdisk ]; then fi sed -i s!%%INSTALLDISK%%!/dev/$(cat /tmp/installdisk)! /autoinstall.yaml if [ "$cryptboot" != "" ] && [ "$cryptboot" != "none" ] && [ "$cryptboot" != "null" ]; then - lukspass=$(head -c 66 < /dev/urandom |base64 -w0) + lukspass=$(python3 /opt/confluent/bin/apiclient /confluent-api/self/profileprivate/pending/luks.key 2> /dev/null) + if [ -z "$lukspass" ]; then + lukspass=$(head -c 66 < /dev/urandom |base64 -w0) + fi export lukspass run_remote_python addcrypt if ! grep 'password:' /autoinstall.yaml > /dev/null; then @@ -52,7 +55,7 @@ if [ "$cryptboot" != "" ] && [ "$cryptboot" != "none" ] && [ "$cryptboot" != "n sed -i s!%%CRYPTPASS%%!$lukspass! /autoinstall.yaml sed -i s!'#CRYPTBOOT'!! /autoinstall.yaml echo -n $lukspass > /etc/confluent_lukspass - + chmod 000 /etc/confluent_lukspass fi ) & tail --pid $! -n 0 -F /var/log/confluent/confluent-pre.log > /dev/console From e6dc383d2598b5c6a9d851b9ed7b5894a25e0532 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 29 Jul 2024 11:22:07 -0400 Subject: [PATCH 074/146] Fix mistake in EL8/EL9 LUKS --- confluent_osdeploy/el8/profiles/default/scripts/tpm_luks.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_osdeploy/el8/profiles/default/scripts/tpm_luks.sh b/confluent_osdeploy/el8/profiles/default/scripts/tpm_luks.sh index c457ffd4..359c46f6 100644 --- a/confluent_osdeploy/el8/profiles/default/scripts/tpm_luks.sh +++ b/confluent_osdeploy/el8/profiles/default/scripts/tpm_luks.sh @@ -1,5 +1,5 @@ #!/bin/sh cryptdisk=$(blkid -t TYPE="crypto_LUKS"|sed -e s/:.*//) -clevis luks bind -f -d $cryptdisk -k /etc/cofluent/luks.key tpm2 '{}' +clevis luks bind -f -d $cryptdisk -k - tpm2 '{}' < /etc/confluent/luks.key chmod 000 /etc/confluent/luks.key #cryptsetup luksRemoveKey $cryptdisk < /etc/confluent/confluent.apikey From 1c4f1ae8175bcd03c8aa0e2bae6507ad23466ceb Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 29 Jul 2024 15:21:10 -0400 Subject: [PATCH 075/146] Try to add ntp and timezones to Ubuntu scripted install --- .../profiles/default/scripts/mergetime | 26 +++++++++++++++++++ .../profiles/default/scripts/pre.sh | 1 + 2 files changed, 27 insertions(+) create mode 100644 confluent_osdeploy/ubuntu22.04/profiles/default/scripts/mergetime diff --git a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/mergetime b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/mergetime new file mode 100644 index 00000000..0cacc1e8 --- /dev/null +++ b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/mergetime @@ -0,0 +1,26 @@ +#!/usr/bin/python3 +import yaml +import os + +ainst = {} +with open('/autoinstall.yaml', 'r') as allin: + ainst = yaml.safe_load(allin) + +tz = None +ntps = [] +with open('/etc/confluent/confluent.deploycfg', 'r') as confluentdeploycfg: + dcfg = yaml.safe_load(confluentdeploycfg) + tz = dcfg['timezone'] + ntps = dcfg.get('ntpservers', []) + +if ntps and not ainst.get('ntp', None): + ainst['ntp'] = {} + ainst['ntp']['enabled'] = True + ainst['servers'] = ntps + +if tz and not ainst.get('timezone'): + ainst['timezone'] = tz + +with open('/autoinstall.yaml', 'w') as allout: + yaml.safe_dump(ainst, allout) + diff --git a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh index 5b609565..ad55120a 100755 --- a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh +++ b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/pre.sh @@ -40,6 +40,7 @@ if [ ! -e /tmp/installdisk ]; then python3 /custom-installation/getinstalldisk fi sed -i s!%%INSTALLDISK%%!/dev/$(cat /tmp/installdisk)! /autoinstall.yaml +run_remote_python mergetime if [ "$cryptboot" != "" ] && [ "$cryptboot" != "none" ] && [ "$cryptboot" != "null" ]; then lukspass=$(python3 /opt/confluent/bin/apiclient /confluent-api/self/profileprivate/pending/luks.key 2> /dev/null) if [ -z "$lukspass" ]; then From 71ca9ef76c7abed0752931d1545b447288f3b7c0 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 29 Jul 2024 15:57:34 -0400 Subject: [PATCH 076/146] Fix path to ntp servers in user-data mod for ubuntu --- .../ubuntu22.04/profiles/default/scripts/mergetime | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/mergetime b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/mergetime index 0cacc1e8..7edb2632 100644 --- a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/mergetime +++ b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/mergetime @@ -16,7 +16,7 @@ with open('/etc/confluent/confluent.deploycfg', 'r') as confluentdeploycfg: if ntps and not ainst.get('ntp', None): ainst['ntp'] = {} ainst['ntp']['enabled'] = True - ainst['servers'] = ntps + ainst['ntp']['servers'] = ntps if tz and not ainst.get('timezone'): ainst['timezone'] = tz From 89bd7c6053c0cd688e9b009b3cb5f07d58a5ecae Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 1 Aug 2024 09:40:39 -0400 Subject: [PATCH 077/146] Force load IB/OPA modules in case of IB boot Ubuntu diskless was not working with boot over IB --- .../initramfs/scripts/init-premount/confluent | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/confluent_osdeploy/ubuntu20.04-diskless/initramfs/scripts/init-premount/confluent b/confluent_osdeploy/ubuntu20.04-diskless/initramfs/scripts/init-premount/confluent index 2f7094b9..a4ca41cf 100644 --- a/confluent_osdeploy/ubuntu20.04-diskless/initramfs/scripts/init-premount/confluent +++ b/confluent_osdeploy/ubuntu20.04-diskless/initramfs/scripts/init-premount/confluent @@ -58,6 +58,10 @@ if ! grep console= /proc/cmdline > /dev/null; then echo "Automatic console configured for $autocons" fi echo sshd:x:30:30:SSH User:/var/empty/sshd:/sbin/nologin >> /etc/passwd +modprobe ib_ipoib +modprobe ib_umad +modprobe hfi1 +modprobe mlx5_ib cd /sys/class/net for nic in *; do ip link set $nic up From acce4de739c54bd1dc7c87bac85617274da0971f Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 2 Aug 2024 11:57:04 -0400 Subject: [PATCH 078/146] Add support for an OpenBMC modification While stock OpenBmc does not care about subprotocols, some implementations use it as a carrier for the XSRF-TOKEN. Since base OpenBmc ignores it, we just offer it to any implementation just in case. --- confluent_server/confluent/plugins/console/openbmc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_server/confluent/plugins/console/openbmc.py b/confluent_server/confluent/plugins/console/openbmc.py index 17acae7c..8677ce17 100644 --- a/confluent_server/confluent/plugins/console/openbmc.py +++ b/confluent_server/confluent/plugins/console/openbmc.py @@ -141,7 +141,7 @@ class TsmConsole(conapi.Console): bmc = prefix + ']' self.ws = WrappedWebSocket(host=bmc) self.ws.set_verify_callback(kv) - self.ws.connect('wss://{0}/console0'.format(self.bmc), host=bmc, cookie='XSRF-TOKEN={0}; SESSION={1}'.format(wc.cookies['XSRF-TOKEN'], wc.cookies['SESSION'])) + self.ws.connect('wss://{0}/console0'.format(self.bmc), host=bmc, cookie='XSRF-TOKEN={0}; SESSION={1}'.format(wc.cookies['XSRF-TOKEN'], wc.cookies['SESSION']), subprotocols=[wc.cookies['XSRF-TOKEN']]) self.connected = True eventlet.spawn_n(self.recvdata) return From 4b6d41d2f82e1b0935542f9173a82705f8da1f6e Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 2 Aug 2024 17:35:39 -0400 Subject: [PATCH 079/146] Begin work to support V4 Lenovo servers V4 Lenovo servers will have XCC3, and will have differences and mark an unambiguously redfish capable onboarding process. For now identify XCC3 variants and mark them, stubbing them to the xcc handler. An XCC3 handler will be made basing on the generic redfishbmc handler with accomodations for XCC specific data (e.g. DeviceDescription attributes and the Lenovo default user/password choice). --- confluent_server/confluent/discovery/core.py | 3 +++ .../confluent/discovery/protocols/ssdp.py | 11 +++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/confluent_server/confluent/discovery/core.py b/confluent_server/confluent/discovery/core.py index dfb50b9f..bb4c99df 100644 --- a/confluent_server/confluent/discovery/core.py +++ b/confluent_server/confluent/discovery/core.py @@ -113,6 +113,7 @@ nodehandlers = { 'service:lenovo-smm': smm, 'service:lenovo-smm2': smm, 'lenovo-xcc': xcc, + 'lenovo-xcc3': xcc, 'service:management-hardware.IBM:integrated-management-module2': imm, 'pxe-client': pxeh, 'onie-switch': None, @@ -132,6 +133,7 @@ servicenames = { 'service:lenovo-smm2': 'lenovo-smm2', 'affluent-switch': 'affluent-switch', 'lenovo-xcc': 'lenovo-xcc', + 'lenovo-xcc3': 'lenovo-xcc3', #'openbmc': 'openbmc', 'service:management-hardware.IBM:integrated-management-module2': 'lenovo-imm2', 'service:io-device.Lenovo:management-module': 'lenovo-switch', @@ -147,6 +149,7 @@ servicebyname = { 'lenovo-smm2': 'service:lenovo-smm2', 'affluent-switch': 'affluent-switch', 'lenovo-xcc': 'lenovo-xcc', + 'lenovo-xcc3': 'lenovo-xcc3', 'lenovo-imm2': 'service:management-hardware.IBM:integrated-management-module2', 'lenovo-switch': 'service:io-device.Lenovo:management-module', 'thinkagile-storage': 'service:thinkagile-storagebmc', diff --git a/confluent_server/confluent/discovery/protocols/ssdp.py b/confluent_server/confluent/discovery/protocols/ssdp.py index 12ec4ba7..34b4f6d0 100644 --- a/confluent_server/confluent/discovery/protocols/ssdp.py +++ b/confluent_server/confluent/discovery/protocols/ssdp.py @@ -431,18 +431,25 @@ def check_fish(urldata, port=443, verifycallback=None): url, data = urldata try: wc = webclient.SecureHTTPConnection(_get_svrip(data), port, verifycallback=verifycallback, timeout=1.5) - peerinfo = wc.grab_json_response(url) + peerinfo = wc.grab_json_response(url, headers={'Accept': 'application/json'}) except socket.error: return None if url == '/DeviceDescription.json': + if not peerinfo: + return None try: peerinfo = peerinfo[0] + except KeyError: + peerinfo['xcc-variant'] = '3' + except IndexError: ++ return None + try: myuuid = peerinfo['node-uuid'].lower() if '-' not in myuuid: myuuid = '-'.join([myuuid[:8], myuuid[8:12], myuuid[12:16], myuuid[16:20], myuuid[20:]]) data['uuid'] = myuuid data['attributes'] = peerinfo - data['services'] = ['lenovo-xcc'] + data['services'] = ['lenovo-xcc'] if 'xcc-variant' not in peerinfo else ['lenovo-xcc' + peerinfo['xcc-variant']] return data except (IndexError, KeyError): return None From e9d4174ce5372e3538a539b3661d200ab04202d8 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 5 Aug 2024 08:35:10 -0400 Subject: [PATCH 080/146] Reapply "Add MegaRAC discovery support for recent MegaRAC" This reverts commit 9d979256eb2c8f96e6a2c334beb57a504eb30f02. --- confluent_server/confluent/discovery/core.py | 9 +- .../confluent/discovery/handlers/megarac.py | 51 ++++ .../discovery/handlers/redfishbmc.py | 269 ++++++++++++++++++ .../confluent/discovery/protocols/ssdp.py | 52 +++- 4 files changed, 366 insertions(+), 15 deletions(-) create mode 100644 confluent_server/confluent/discovery/handlers/megarac.py create mode 100644 confluent_server/confluent/discovery/handlers/redfishbmc.py diff --git a/confluent_server/confluent/discovery/core.py b/confluent_server/confluent/discovery/core.py index bb4c99df..b734cece 100644 --- a/confluent_server/confluent/discovery/core.py +++ b/confluent_server/confluent/discovery/core.py @@ -74,6 +74,7 @@ import confluent.discovery.handlers.tsm as tsm import confluent.discovery.handlers.pxe as pxeh import confluent.discovery.handlers.smm as smm import confluent.discovery.handlers.xcc as xcc +import confluent.discovery.handlers.megarac as megarac import confluent.exceptions as exc import confluent.log as log import confluent.messages as msg @@ -114,6 +115,7 @@ nodehandlers = { 'service:lenovo-smm2': smm, 'lenovo-xcc': xcc, 'lenovo-xcc3': xcc, + 'megarac-bmc': megarac, 'service:management-hardware.IBM:integrated-management-module2': imm, 'pxe-client': pxeh, 'onie-switch': None, @@ -134,6 +136,7 @@ servicenames = { 'affluent-switch': 'affluent-switch', 'lenovo-xcc': 'lenovo-xcc', 'lenovo-xcc3': 'lenovo-xcc3', + 'megarac-bmc': 'megarac-bmc', #'openbmc': 'openbmc', 'service:management-hardware.IBM:integrated-management-module2': 'lenovo-imm2', 'service:io-device.Lenovo:management-module': 'lenovo-switch', @@ -150,6 +153,7 @@ servicebyname = { 'affluent-switch': 'affluent-switch', 'lenovo-xcc': 'lenovo-xcc', 'lenovo-xcc3': 'lenovo-xcc3', + 'megarac-bmc': 'megarac-bmc', 'lenovo-imm2': 'service:management-hardware.IBM:integrated-management-module2', 'lenovo-switch': 'service:io-device.Lenovo:management-module', 'thinkagile-storage': 'service:thinkagile-storagebmc', @@ -456,7 +460,7 @@ def iterate_addrs(addrs, countonly=False): yield 1 return yield addrs - + def _parameterize_path(pathcomponents): listrequested = False childcoll = True @@ -545,7 +549,7 @@ def handle_api_request(configmanager, inputdata, operation, pathcomponents): if len(pathcomponents) > 2: raise Exception('TODO') currsubs = get_subscriptions() - return [msg.ChildCollection(x) for x in currsubs] + return [msg.ChildCollection(x) for x in currsubs] elif operation == 'retrieve': return handle_read_api_request(pathcomponents) elif (operation in ('update', 'create') and @@ -1706,3 +1710,4 @@ if __name__ == '__main__': start_detection() while True: eventlet.sleep(30) + diff --git a/confluent_server/confluent/discovery/handlers/megarac.py b/confluent_server/confluent/discovery/handlers/megarac.py new file mode 100644 index 00000000..d7d8786a --- /dev/null +++ b/confluent_server/confluent/discovery/handlers/megarac.py @@ -0,0 +1,51 @@ +# Copyright 2024 Lenovo +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import confluent.discovery.handlers.redfishbmc as redfishbmc +import eventlet.support.greendns + + +getaddrinfo = eventlet.support.greendns.getaddrinfo + + +class NodeHandler(redfishbmc.NodeHandler): + + def get_firmware_default_account_info(self): + return ('admin', 'admin') + + +def remote_nodecfg(nodename, cfm): + cfg = cfm.get_node_attributes( + nodename, 'hardwaremanagement.manager') + ipaddr = cfg.get(nodename, {}).get('hardwaremanagement.manager', {}).get( + 'value', None) + ipaddr = ipaddr.split('/', 1)[0] + ipaddr = getaddrinfo(ipaddr, 0)[0][-1] + if not ipaddr: + raise Exception('Cannot remote configure a system without known ' + 'address') + info = {'addresses': [ipaddr]} + nh = NodeHandler(info, cfm) + nh.config(nodename) + + +if __name__ == '__main__': + import confluent.config.configmanager as cfm + c = cfm.ConfigManager(None) + import sys + info = {'addresses': [[sys.argv[1]]]} + print(repr(info)) + testr = NodeHandler(info, c) + testr.config(sys.argv[2]) + diff --git a/confluent_server/confluent/discovery/handlers/redfishbmc.py b/confluent_server/confluent/discovery/handlers/redfishbmc.py new file mode 100644 index 00000000..eed401de --- /dev/null +++ b/confluent_server/confluent/discovery/handlers/redfishbmc.py @@ -0,0 +1,269 @@ +# Copyright 2024 Lenovo +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import confluent.discovery.handlers.generic as generic +import confluent.exceptions as exc +import confluent.netutil as netutil +import confluent.util as util +import eventlet +import eventlet.support.greendns +import json +try: + from urllib import urlencode +except ImportError: + from urllib.parse import urlencode + +getaddrinfo = eventlet.support.greendns.getaddrinfo + +webclient = eventlet.import_patched('pyghmi.util.webclient') + +def get_host_interface_urls(wc, mginfo): + returls = [] + hifurl = mginfo.get('HostInterfaces', {}).get('@odata.id', None) + if not hifurl: + return None + hifinfo = wc.grab_json_response(hifurl) + hifurls = hifinfo.get('Members', []) + for hifurl in hifurls: + hifurl = hifurl['@odata.id'] + hifinfo = wc.grab_json_response(hifurl) + acturl = hifinfo.get('ManagerEthernetInterface', {}).get('@odata.id', None) + if acturl: + returls.append(acturl) + return returls + + +class NodeHandler(generic.NodeHandler): + devname = 'BMC' + + def __init__(self, info, configmanager): + self.trieddefault = None + self.targuser = None + self.curruser = None + self.currpass = None + self.targpass = None + self.nodename = None + self.csrftok = None + self.channel = None + self.atdefault = True + super(NodeHandler, self).__init__(info, configmanager) + + def get_firmware_default_account_info(self): + raise Exception('This must be subclassed') + + def scan(self): + c = webclient.SecureHTTPConnection(self.ipaddr, 443, verifycallback=self.validate_cert) + i = c.grab_json_response('/redfish/v1/') + uuid = i.get('UUID', None) + if uuid: + self.info['uuid'] = uuid.lower() + + def validate_cert(self, certificate): + # broadly speaking, merely checks consistency moment to moment, + # but if https_cert gets stricter, this check means something + fprint = util.get_fingerprint(self.https_cert) + return util.cert_matches(fprint, certificate) + + def _get_wc(self): + defuser, defpass = self.get_firmware_default_account_info() + wc = webclient.SecureHTTPConnection(self.ipaddr, 443, verifycallback=self.validate_cert) + wc.set_basic_credentials(defuser, defpass) + wc.set_header('Content-Type', 'application/json') + authmode = 0 + if not self.trieddefault: + rsp, status = wc.grab_json_response_with_status('/redfish/v1/Managers') + if status == 403: + self.trieddefault = True + chgurl = None + rsp = json.loads(rsp) + currerr = rsp.get('error', {}) + ecode = currerr.get('code', None) + if ecode.endswith('PasswordChangeRequired'): + for einfo in currerr.get('@Message.ExtendedInfo', []): + if einfo.get('MessageId', None).endswith('PasswordChangeRequired'): + for msgarg in einfo.get('MessageArgs'): + chgurl = msgarg + break + if chgurl: + if self.targpass == defpass: + raise Exception("Must specify a non-default password to onboard this BMC") + wc.set_header('If-Match', '*') + cpr = wc.grab_json_response_with_status(chgurl, {'Password': self.targpass}, method='PATCH') + if cpr[1] >= 200 and cpr[1] < 300: + self.curruser = defuser + self.currpass = self.targpass + wc.set_basic_credentials(self.curruser, self.currpass) + _, status = wc.grab_json_response_with_status('/redfish/v1/Managers') + tries = 10 + while status >= 300 and tries: + eventlet.sleep(1) + _, status = wc.grab_json_response_with_status('/redfish/v1/Managers') + return wc + + if status > 400: + self.trieddefault = True + if status == 401: + wc.set_basic_credentials(self.DEFAULT_USER, self.targpass) + rsp, status = wc.grab_json_response_with_status('/redfish/v1/Managers') + if status == 200: # Default user still, but targpass + self.currpass = self.targpass + self.curruser = defuser + return wc + elif self.targuser != defuser: + wc.set_basic_credentials(self.targuser, self.targpass) + rsp, status = wc.grab_json_response_with_status('/redfish/v1/Managers') + if status != 200: + raise Exception("Target BMC does not recognize firmware default credentials nor the confluent stored credential") + else: + self.curruser = defuser + self.currpass = defpass + return wc + if self.curruser: + wc.set_basic_credentials(self.curruser, self.currpass) + rsp, status = wc.grab_json_response_with_status('/redfish/v1/Managers') + if status != 200: + return None + return wc + wc.set_basic_credentials(self.targuser, self.targpass) + rsp, status = wc.grab_json_response_with_status('/redfish/v1/Managers') + if status != 200: + return None + self.curruser = self.targuser + self.currpass = self.targpass + return wc + + def config(self, nodename): + self.nodename = nodename + creds = self.configmanager.get_node_attributes( + nodename, ['secret.hardwaremanagementuser', + 'secret.hardwaremanagementpassword', + 'hardwaremanagement.manager', 'hardwaremanagement.method', 'console.method'], + True) + cd = creds.get(nodename, {}) + defuser, defpass = self.get_firmware_default_account_info() + user, passwd, _ = self.get_node_credentials( + nodename, creds, defuser, defpass) + user = util.stringify(user) + passwd = util.stringify(passwd) + self.targuser = user + self.targpass = passwd + wc = self._get_wc() + srvroot, status = wc.grab_json_response_with_status('/redfish/v1/') + curruserinfo = {} + authupdate = {} + wc.set_header('Content-Type', 'application/json') + if user != self.curruser: + authupdate['UserName'] = user + if passwd != self.currpass: + authupdate['Password'] = passwd + if authupdate: + targaccturl = None + asrv = srvroot.get('AccountService', {}).get('@odata.id') + rsp, status = wc.grab_json_response_with_status(asrv) + accts = rsp.get('Accounts', {}).get('@odata.id') + rsp, status = wc.grab_json_response_with_status(accts) + accts = rsp.get('Members', []) + for accturl in accts: + accturl = accturl.get('@odata.id', '') + if accturl: + rsp, status = wc.grab_json_response_with_status(accturl) + if rsp.get('UserName', None) == self.curruser: + targaccturl = accturl + break + else: + raise Exception("Unable to identify Account URL to modify on this BMC") + rsp, status = wc.grab_json_response_with_status(targaccturl, authupdate, method='PATCH') + if status >= 300: + raise Exception("Failed attempting to update credentials on BMC") + wc.set_basic_credentials(user, passwd) + _, status = wc.grab_json_response_with_status('/redfish/v1/Managers') + tries = 10 + while tries and status >= 300: + tries -= 1 + eventlet.sleep(1.0) + _, status = wc.grab_json_response_with_status('/redfish/v1/Managers') + if ('hardwaremanagement.manager' in cd and + cd['hardwaremanagement.manager']['value'] and + not cd['hardwaremanagement.manager']['value'].startswith( + 'fe80::')): + newip = cd['hardwaremanagement.manager']['value'] + newip = newip.split('/', 1)[0] + newipinfo = getaddrinfo(newip, 0)[0] + newip = newipinfo[-1][0] + if ':' in newip: + raise exc.NotImplementedException('IPv6 remote config TODO') + mgrs = srvroot['Managers']['@odata.id'] + rsp = wc.grab_json_response(mgrs) + if len(rsp['Members']) != 1: + raise Exception("Can not handle multiple Managers") + mgrurl = rsp['Members'][0]['@odata.id'] + mginfo = wc.grab_json_response(mgrurl) + hifurls = get_host_interface_urls(wc, mginfo) + mgtnicinfo = mginfo['EthernetInterfaces']['@odata.id'] + mgtnicinfo = wc.grab_json_response(mgtnicinfo) + mgtnics = [x['@odata.id'] for x in mgtnicinfo.get('Members', [])] + actualnics = [] + for candnic in mgtnics: + if candnic in hifurls: + continue + actualnics.append(candnic) + if len(actualnics) != 1: + raise Exception("Multi-interface BMCs are not supported currently") + currnet = wc.grab_json_response(actualnics[0]) + netconfig = netutil.get_nic_config(self.configmanager, nodename, ip=newip) + newconfig = { + "Address": newip, + "SubnetMask": netutil.cidr_to_mask(netconfig['prefix']), + } + newgw = netconfig['ipv4_gateway'] + if newgw: + newconfig['Gateway'] = newgw + else: + newconfig['Gateway'] = newip # required property, set to self just to have a value + for net in currnet.get("IPv4Addresses", []): + if net["Address"] == newip and net["SubnetMask"] == newconfig['SubnetMask'] and (not newgw or newconfig['Gateway'] == newgw): + break + else: + wc.set_header('If-Match', '*') + rsp, status = wc.grab_json_response_with_status(actualnics[0], {'IPv4StaticAddresses': [newconfig]}, method='PATCH') + elif self.ipaddr.startswith('fe80::'): + self.configmanager.set_node_attributes( + {nodename: {'hardwaremanagement.manager': self.ipaddr}}) + else: + raise exc.TargetEndpointUnreachable( + 'hardwaremanagement.manager must be set to desired address (No IPv6 Link Local detected)') + + +def remote_nodecfg(nodename, cfm): + cfg = cfm.get_node_attributes( + nodename, 'hardwaremanagement.manager') + ipaddr = cfg.get(nodename, {}).get('hardwaremanagement.manager', {}).get( + 'value', None) + ipaddr = ipaddr.split('/', 1)[0] + ipaddr = getaddrinfo(ipaddr, 0)[0][-1] + if not ipaddr: + raise Exception('Cannot remote configure a system without known ' + 'address') + info = {'addresses': [ipaddr]} + nh = NodeHandler(info, cfm) + nh.config(nodename) + +if __name__ == '__main__': + import confluent.config.configmanager as cfm + c = cfm.ConfigManager(None) + import sys + info = {'addresses': [[sys.argv[1]]] } + print(repr(info)) + testr = NodeHandler(info, c) + testr.config(sys.argv[2]) diff --git a/confluent_server/confluent/discovery/protocols/ssdp.py b/confluent_server/confluent/discovery/protocols/ssdp.py index 34b4f6d0..45d1e1f3 100644 --- a/confluent_server/confluent/discovery/protocols/ssdp.py +++ b/confluent_server/confluent/discovery/protocols/ssdp.py @@ -60,6 +60,7 @@ def active_scan(handler, protocol=None): known_peers = set([]) for scanned in scan(['urn:dmtf-org:service:redfish-rest:1', 'urn::service:affluent']): for addr in scanned['addresses']: + addr = addr[0:1] + addr[2:] if addr in known_peers: break hwaddr = neighutil.get_hwaddr(addr[0]) @@ -79,13 +80,20 @@ def scan(services, target=None): def _process_snoop(peer, rsp, mac, known_peers, newmacs, peerbymacaddress, byehandler, machandlers, handler): - if mac in peerbymacaddress and peer not in peerbymacaddress[mac]['addresses']: - peerbymacaddress[mac]['addresses'].append(peer) + if mac in peerbymacaddress: + normpeer = peer[0:1] + peer[2:] + for currpeer in peerbymacaddress[mac]['addresses']: + currnormpeer = currpeer[0:1] + peer[2:] + if currnormpeer == normpeer: + break + else: + peerbymacaddress[mac]['addresses'].append(peer) else: peerdata = { 'hwaddr': mac, 'addresses': [peer], } + targurl = None for headline in rsp[1:]: if not headline: continue @@ -105,13 +113,20 @@ def _process_snoop(peer, rsp, mac, known_peers, newmacs, peerbymacaddress, byeha if not value.endswith('/redfish/v1/'): return elif header == 'LOCATION': - if not value.endswith('/DeviceDescription.json'): + if '/eth' in value and value.endswith('.xml'): + targurl = '/redfish/v1/' + targtype = 'megarac-bmc' + continue # MegaRAC redfish + elif value.endswith('/DeviceDescription.json'): + targurl = '/DeviceDescription.json' + targtype = 'megarac-bmc' + else: return - if handler: - eventlet.spawn_n(check_fish_handler, handler, peerdata, known_peers, newmacs, peerbymacaddress, machandlers, mac, peer) + if handler and targurl: + eventlet.spawn_n(check_fish_handler, handler, peerdata, known_peers, newmacs, peerbymacaddress, machandlers, mac, peer, targurl, targtype) -def check_fish_handler(handler, peerdata, known_peers, newmacs, peerbymacaddress, machandlers, mac, peer): - retdata = check_fish(('/DeviceDescription.json', peerdata)) +def check_fish_handler(handler, peerdata, known_peers, newmacs, peerbymacaddress, machandlers, mac, peer, targurl, targtype): + retdata = check_fish((targurl, peerdata, targtype)) if retdata: known_peers.add(peer) newmacs.add(mac) @@ -325,7 +340,7 @@ def _find_service(service, target): host = '[{0}]'.format(host) msg = smsg.format(host, service) if not isinstance(msg, bytes): - msg = msg.encode('utf8') + msg = msg.encode('utf8') net6.sendto(msg, addr[4]) else: net4.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) @@ -413,7 +428,11 @@ def _find_service(service, target): if '/redfish/v1/' not in peerdata[nid].get('urls', ()) and '/redfish/v1' not in peerdata[nid].get('urls', ()): continue if '/DeviceDescription.json' in peerdata[nid]['urls']: - pooltargs.append(('/DeviceDescription.json', peerdata[nid])) + pooltargs.append(('/DeviceDescription.json', peerdata[nid], 'lenovo-xcc')) + else: + for targurl in peerdata[nid]['urls']: + if '/eth' in targurl and targurl.endswith('.xml'): + pooltargs.append(('/redfish/v1/', peerdata[nid], 'megarac-bmc')) # For now, don't interrogate generic redfish bmcs # This is due to a need to deduplicate from some supported SLP # targets (IMM, TSM, others) @@ -428,7 +447,7 @@ def _find_service(service, target): def check_fish(urldata, port=443, verifycallback=None): if not verifycallback: verifycallback = lambda x: True - url, data = urldata + url, data, targtype = urldata try: wc = webclient.SecureHTTPConnection(_get_svrip(data), port, verifycallback=verifycallback, timeout=1.5) peerinfo = wc.grab_json_response(url, headers={'Accept': 'application/json'}) @@ -457,7 +476,7 @@ def check_fish(urldata, port=443, verifycallback=None): peerinfo = wc.grab_json_response('/redfish/v1/') if url == '/redfish/v1/': if 'UUID' in peerinfo: - data['services'] = ['service:redfish-bmc'] + data['services'] = [targtype] data['uuid'] = peerinfo['UUID'].lower() return data return None @@ -476,7 +495,12 @@ def _parse_ssdp(peer, rsp, peerdata): if code == b'200': if nid in peerdata: peerdatum = peerdata[nid] - if peer not in peerdatum['addresses']: + normpeer = peer[0:1] + peer[2:] + for currpeer in peerdatum['addresses']: + currnormpeer = currpeer[0:1] + peer[2:] + if currnormpeer == normpeer: + break + else: peerdatum['addresses'].append(peer) else: peerdatum = { @@ -511,5 +535,7 @@ def _parse_ssdp(peer, rsp, peerdata): if __name__ == '__main__': def printit(rsp): - print(repr(rsp)) + pass # print(repr(rsp)) active_scan(printit) + + From cfb31a0d8dbc94df1e481c0c673d94e563eb1a03 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 5 Aug 2024 10:00:22 -0400 Subject: [PATCH 081/146] Implement XCC3 discovery For XCC3, change to generic redfish onboarding mechanism. Extend the generic mechanism to be more specific in some ways that the XCC3 is pickier about. However, it's just reiteration of what should have already have been the case. --- confluent_server/confluent/discovery/core.py | 3 +- .../discovery/handlers/redfishbmc.py | 8 +- .../confluent/discovery/handlers/xcc3.py | 102 ++++++++++++++++++ 3 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 confluent_server/confluent/discovery/handlers/xcc3.py diff --git a/confluent_server/confluent/discovery/core.py b/confluent_server/confluent/discovery/core.py index b734cece..fd302f8b 100644 --- a/confluent_server/confluent/discovery/core.py +++ b/confluent_server/confluent/discovery/core.py @@ -74,6 +74,7 @@ import confluent.discovery.handlers.tsm as tsm import confluent.discovery.handlers.pxe as pxeh import confluent.discovery.handlers.smm as smm import confluent.discovery.handlers.xcc as xcc +import confluent.discovery.handlers.xcc3 as xcc3 import confluent.discovery.handlers.megarac as megarac import confluent.exceptions as exc import confluent.log as log @@ -114,7 +115,7 @@ nodehandlers = { 'service:lenovo-smm': smm, 'service:lenovo-smm2': smm, 'lenovo-xcc': xcc, - 'lenovo-xcc3': xcc, + 'lenovo-xcc3': xcc3, 'megarac-bmc': megarac, 'service:management-hardware.IBM:integrated-management-module2': imm, 'pxe-client': pxeh, diff --git a/confluent_server/confluent/discovery/handlers/redfishbmc.py b/confluent_server/confluent/discovery/handlers/redfishbmc.py index eed401de..97629f36 100644 --- a/confluent_server/confluent/discovery/handlers/redfishbmc.py +++ b/confluent_server/confluent/discovery/handlers/redfishbmc.py @@ -80,6 +80,7 @@ class NodeHandler(generic.NodeHandler): wc = webclient.SecureHTTPConnection(self.ipaddr, 443, verifycallback=self.validate_cert) wc.set_basic_credentials(defuser, defpass) wc.set_header('Content-Type', 'application/json') + wc.set_header('Accept', 'application/json') authmode = 0 if not self.trieddefault: rsp, status = wc.grab_json_response_with_status('/redfish/v1/Managers') @@ -114,7 +115,7 @@ class NodeHandler(generic.NodeHandler): if status > 400: self.trieddefault = True if status == 401: - wc.set_basic_credentials(self.DEFAULT_USER, self.targpass) + wc.set_basic_credentials(defuser, self.targpass) rsp, status = wc.grab_json_response_with_status('/redfish/v1/Managers') if status == 200: # Default user still, but targpass self.currpass = self.targpass @@ -236,7 +237,10 @@ class NodeHandler(generic.NodeHandler): break else: wc.set_header('If-Match', '*') - rsp, status = wc.grab_json_response_with_status(actualnics[0], {'IPv4StaticAddresses': [newconfig]}, method='PATCH') + rsp, status = wc.grab_json_response_with_status(actualnics[0], { + 'DHCPv4': {'DHCPEnabled': False}, + 'IPv4StaticAddresses': [newconfig]}, method='PATCH') + elif self.ipaddr.startswith('fe80::'): self.configmanager.set_node_attributes( {nodename: {'hardwaremanagement.manager': self.ipaddr}}) diff --git a/confluent_server/confluent/discovery/handlers/xcc3.py b/confluent_server/confluent/discovery/handlers/xcc3.py new file mode 100644 index 00000000..780de4fc --- /dev/null +++ b/confluent_server/confluent/discovery/handlers/xcc3.py @@ -0,0 +1,102 @@ +# Copyright 2024 Lenovo +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import confluent.discovery.handlers.redfishbmc as redfishbmc +import eventlet.support.greendns +import confluent.util as util + +webclient = eventlet.import_patched('pyghmi.util.webclient') + + + +getaddrinfo = eventlet.support.greendns.getaddrinfo + + +class NodeHandler(redfishbmc.NodeHandler): + + def get_firmware_default_account_info(self): + return ('USERID', 'PASSW0RD') + + def scan(self): + ip, port = self.get_web_port_and_ip() + c = webclient.SecureHTTPConnection(ip, port, + verifycallback=self.validate_cert) + i = c.grab_json_response('/api/providers/logoninfo') + modelname = i.get('items', [{}])[0].get('machine_name', None) + if modelname: + self.info['modelname'] = modelname + for attrname in list(self.info.get('attributes', {})): + val = self.info['attributes'][attrname] + if '-uuid' == attrname[-5:] and len(val) == 32: + val = val.lower() + self.info['attributes'][attrname] = '-'.join([val[:8], val[8:12], val[12:16], val[16:20], val[20:]]) + attrs = self.info.get('attributes', {}) + room = attrs.get('room-id', None) + if room: + self.info['room'] = room + rack = attrs.get('rack-id', None) + if rack: + self.info['rack'] = rack + name = attrs.get('name', None) + if name: + self.info['hostname'] = name + unumber = attrs.get('lowest-u', None) + if unumber: + self.info['u'] = unumber + location = attrs.get('location', None) + if location: + self.info['location'] = location + mtm = attrs.get('enclosure-machinetype-model', None) + if mtm: + self.info['modelnumber'] = mtm.strip() + sn = attrs.get('enclosure-serial-number', None) + if sn: + self.info['serialnumber'] = sn.strip() + if attrs.get('enclosure-form-factor', None) == 'dense-computing': + encuuid = attrs.get('chassis-uuid', None) + if encuuid: + self.info['enclosure.uuid'] = fixuuid(encuuid) + slot = int(attrs.get('slot', 0)) + if slot != 0: + self.info['enclosure.bay'] = slot + + def validate_cert(self, certificate): + fprint = util.get_fingerprint(self.https_cert) + return util.cert_matches(fprint, certificate) + + +def remote_nodecfg(nodename, cfm): + cfg = cfm.get_node_attributes( + nodename, 'hardwaremanagement.manager') + ipaddr = cfg.get(nodename, {}).get('hardwaremanagement.manager', {}).get( + 'value', None) + ipaddr = ipaddr.split('/', 1)[0] + ipaddr = getaddrinfo(ipaddr, 0)[0][-1] + if not ipaddr: + raise Exception('Cannot remote configure a system without known ' + 'address') + info = {'addresses': [ipaddr]} + nh = NodeHandler(info, cfm) + nh.config(nodename) + + +if __name__ == '__main__': + import confluent.config.configmanager as cfm + c = cfm.ConfigManager(None) + import sys + info = {'addresses': [[sys.argv[1]]]} + print(repr(info)) + testr = NodeHandler(info, c) + testr.config(sys.argv[2]) + From 30c4d6b863e74cd42d974867c68a45c5873f3ecc Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 5 Aug 2024 11:07:50 -0400 Subject: [PATCH 082/146] Add IPMI enablement to generic Redfish handler If attributes indicate desire for IPMI, try to accomodate. --- .../discovery/handlers/redfishbmc.py | 102 +++++++++++++----- .../confluent/discovery/handlers/xcc.py | 2 +- .../confluent/discovery/handlers/xcc3.py | 1 + 3 files changed, 76 insertions(+), 29 deletions(-) diff --git a/confluent_server/confluent/discovery/handlers/redfishbmc.py b/confluent_server/confluent/discovery/handlers/redfishbmc.py index 97629f36..d4e164d6 100644 --- a/confluent_server/confluent/discovery/handlers/redfishbmc.py +++ b/confluent_server/confluent/discovery/handlers/redfishbmc.py @@ -57,8 +57,28 @@ class NodeHandler(generic.NodeHandler): self.csrftok = None self.channel = None self.atdefault = True + self._srvroot = None + self._mgrinfo = None super(NodeHandler, self).__init__(info, configmanager) + def srvroot(self, wc): + if not self._srvroot: + srvroot, status = wc.grab_json_response_with_status('/redfish/v1/') + if status == 200: + self._srvroot = srvroot + return self._srvroot + + def mgrinfo(self, wc): + if not self._mgrinfo: + mgrs = self.srvroot(wc)['Managers']['@odata.id'] + rsp = wc.grab_json_response(mgrs) + if len(rsp['Members']) != 1: + raise Exception("Can not handle multiple Managers") + mgrurl = rsp['Members'][0]['@odata.id'] + self._mgrinfo = wc.grab_json_response(mgrurl) + return self._mgrinfo + + def get_firmware_default_account_info(self): raise Exception('This must be subclassed') @@ -75,6 +95,30 @@ class NodeHandler(generic.NodeHandler): fprint = util.get_fingerprint(self.https_cert) return util.cert_matches(fprint, certificate) + def enable_ipmi(self, wc): + npu = self.mgrinfo(wc).get( + 'NetworkProtocol', {}).get('@odata.id', None) + if not npu: + raise Exception('Cannot enable IPMI, no NetworkProtocol on BMC') + npi = wc.grab_json_response(npu) + if not npi.get('IPMI', {}).get('ProtocolEnabled'): + wc.set_header('If-Match', '*') + wc.grab_json_response_with_status( + npu, {'IPMI': {'ProtocolEnabled': True}}, method='PATCH') + acctinfo = wc.grab_json_response_with_status( + self.target_account_url(wc)) + acctinfo = acctinfo[0] + actypes = acctinfo['AccountTypes'] + candidates = acctinfo['AccountTypes@Redfish.AllowableValues'] + if 'IPMI' not in actypes and 'IPMI' in candidates: + actypes.append('IPMI') + acctupd = { + 'AccountTypes': actypes, + 'Password': self.currpass, + } + rsp = wc.grab_json_response_with_status( + self.target_account_url(wc), acctupd, method='PATCH') + def _get_wc(self): defuser, defpass = self.get_firmware_default_account_info() wc = webclient.SecureHTTPConnection(self.ipaddr, 443, verifycallback=self.validate_cert) @@ -144,13 +188,33 @@ class NodeHandler(generic.NodeHandler): self.currpass = self.targpass return wc + def target_account_url(self, wc): + asrv = self.srvroot(wc).get('AccountService', {}).get('@odata.id') + rsp, status = wc.grab_json_response_with_status(asrv) + accts = rsp.get('Accounts', {}).get('@odata.id') + rsp, status = wc.grab_json_response_with_status(accts) + accts = rsp.get('Members', []) + for accturl in accts: + accturl = accturl.get('@odata.id', '') + if accturl: + rsp, status = wc.grab_json_response_with_status(accturl) + if rsp.get('UserName', None) == self.curruser: + targaccturl = accturl + break + else: + raise Exception("Unable to identify Account URL to modify on this BMC") + return targaccturl + def config(self, nodename): + mgrs = None self.nodename = nodename creds = self.configmanager.get_node_attributes( nodename, ['secret.hardwaremanagementuser', 'secret.hardwaremanagementpassword', - 'hardwaremanagement.manager', 'hardwaremanagement.method', 'console.method'], - True) + 'hardwaremanagement.manager', + 'hardwaremanagement.method', + 'console.method'], + True) cd = creds.get(nodename, {}) defuser, defpass = self.get_firmware_default_account_info() user, passwd, _ = self.get_node_credentials( @@ -160,7 +224,6 @@ class NodeHandler(generic.NodeHandler): self.targuser = user self.targpass = passwd wc = self._get_wc() - srvroot, status = wc.grab_json_response_with_status('/redfish/v1/') curruserinfo = {} authupdate = {} wc.set_header('Content-Type', 'application/json') @@ -169,21 +232,7 @@ class NodeHandler(generic.NodeHandler): if passwd != self.currpass: authupdate['Password'] = passwd if authupdate: - targaccturl = None - asrv = srvroot.get('AccountService', {}).get('@odata.id') - rsp, status = wc.grab_json_response_with_status(asrv) - accts = rsp.get('Accounts', {}).get('@odata.id') - rsp, status = wc.grab_json_response_with_status(accts) - accts = rsp.get('Members', []) - for accturl in accts: - accturl = accturl.get('@odata.id', '') - if accturl: - rsp, status = wc.grab_json_response_with_status(accturl) - if rsp.get('UserName', None) == self.curruser: - targaccturl = accturl - break - else: - raise Exception("Unable to identify Account URL to modify on this BMC") + targaccturl = self.target_account_url(wc) rsp, status = wc.grab_json_response_with_status(targaccturl, authupdate, method='PATCH') if status >= 300: raise Exception("Failed attempting to update credentials on BMC") @@ -193,7 +242,11 @@ class NodeHandler(generic.NodeHandler): while tries and status >= 300: tries -= 1 eventlet.sleep(1.0) - _, status = wc.grab_json_response_with_status('/redfish/v1/Managers') + _, status = wc.grab_json_response_with_status( + '/redfish/v1/Managers') + if (cd.get('hardwaremanagement.method', {}).get('value', 'ipmi') != 'redfish' + or cd.get('console.method', {}).get('value', None) == 'ipmi'): + self.enable_ipmi(wc) if ('hardwaremanagement.manager' in cd and cd['hardwaremanagement.manager']['value'] and not cd['hardwaremanagement.manager']['value'].startswith( @@ -204,14 +257,8 @@ class NodeHandler(generic.NodeHandler): newip = newipinfo[-1][0] if ':' in newip: raise exc.NotImplementedException('IPv6 remote config TODO') - mgrs = srvroot['Managers']['@odata.id'] - rsp = wc.grab_json_response(mgrs) - if len(rsp['Members']) != 1: - raise Exception("Can not handle multiple Managers") - mgrurl = rsp['Members'][0]['@odata.id'] - mginfo = wc.grab_json_response(mgrurl) - hifurls = get_host_interface_urls(wc, mginfo) - mgtnicinfo = mginfo['EthernetInterfaces']['@odata.id'] + hifurls = get_host_interface_urls(wc, self.mgrinfo(wc)) + mgtnicinfo = self.mgrinfo(wc)['EthernetInterfaces']['@odata.id'] mgtnicinfo = wc.grab_json_response(mgtnicinfo) mgtnics = [x['@odata.id'] for x in mgtnicinfo.get('Members', [])] actualnics = [] @@ -240,7 +287,6 @@ class NodeHandler(generic.NodeHandler): rsp, status = wc.grab_json_response_with_status(actualnics[0], { 'DHCPv4': {'DHCPEnabled': False}, 'IPv4StaticAddresses': [newconfig]}, method='PATCH') - elif self.ipaddr.startswith('fe80::'): self.configmanager.set_node_attributes( {nodename: {'hardwaremanagement.manager': self.ipaddr}}) diff --git a/confluent_server/confluent/discovery/handlers/xcc.py b/confluent_server/confluent/discovery/handlers/xcc.py index 49fe1e87..d4d67590 100644 --- a/confluent_server/confluent/discovery/handlers/xcc.py +++ b/confluent_server/confluent/discovery/handlers/xcc.py @@ -639,7 +639,7 @@ def remote_nodecfg(nodename, cfm): ipaddr = ipaddr.split('/', 1)[0] ipaddr = getaddrinfo(ipaddr, 0)[0][-1] if not ipaddr: - raise Excecption('Cannot remote configure a system without known ' + raise Exception('Cannot remote configure a system without known ' 'address') info = {'addresses': [ipaddr]} nh = NodeHandler(info, cfm) diff --git a/confluent_server/confluent/discovery/handlers/xcc3.py b/confluent_server/confluent/discovery/handlers/xcc3.py index 780de4fc..24974172 100644 --- a/confluent_server/confluent/discovery/handlers/xcc3.py +++ b/confluent_server/confluent/discovery/handlers/xcc3.py @@ -24,6 +24,7 @@ getaddrinfo = eventlet.support.greendns.getaddrinfo class NodeHandler(redfishbmc.NodeHandler): + devname = 'XCC' def get_firmware_default_account_info(self): return ('USERID', 'PASSW0RD') From fc5c1aa90f4551e634db7b063f1db5d945683eb2 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 5 Aug 2024 11:32:57 -0400 Subject: [PATCH 083/146] Fix SSDP error during merge --- confluent_server/confluent/discovery/protocols/ssdp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_server/confluent/discovery/protocols/ssdp.py b/confluent_server/confluent/discovery/protocols/ssdp.py index 45d1e1f3..e2688d66 100644 --- a/confluent_server/confluent/discovery/protocols/ssdp.py +++ b/confluent_server/confluent/discovery/protocols/ssdp.py @@ -461,7 +461,7 @@ def check_fish(urldata, port=443, verifycallback=None): except KeyError: peerinfo['xcc-variant'] = '3' except IndexError: -+ return None + return None try: myuuid = peerinfo['node-uuid'].lower() if '-' not in myuuid: From 0fd07e842748d60cc67d125bdbf9540adae569bf Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 5 Aug 2024 13:09:50 -0400 Subject: [PATCH 084/146] Fix race condition in SSDP snoop If an asynchronous handler is slow to enroll a target while another target causes an iteration of the snoop loop, the various modified structures had been discarded in the interim. Now persist the data structures iteration to iteration, using 'clear()' to empty them rather than getting brand new data structures each loop. --- .../confluent/discovery/protocols/ssdp.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/confluent_server/confluent/discovery/protocols/ssdp.py b/confluent_server/confluent/discovery/protocols/ssdp.py index e2688d66..c7063838 100644 --- a/confluent_server/confluent/discovery/protocols/ssdp.py +++ b/confluent_server/confluent/discovery/protocols/ssdp.py @@ -116,10 +116,11 @@ def _process_snoop(peer, rsp, mac, known_peers, newmacs, peerbymacaddress, byeha if '/eth' in value and value.endswith('.xml'): targurl = '/redfish/v1/' targtype = 'megarac-bmc' - continue # MegaRAC redfish + continue # MegaRAC redfish elif value.endswith('/DeviceDescription.json'): targurl = '/DeviceDescription.json' - targtype = 'megarac-bmc' + targtype = 'lenovo-xcc' + continue else: return if handler and targurl: @@ -179,11 +180,14 @@ def snoop(handler, byehandler=None, protocol=None, uuidlookup=None): net4.bind(('', 1900)) net6.bind(('', 1900)) peerbymacaddress = {} + newmacs = set([]) + deferrednotifies = [] + machandlers = {} while True: try: - newmacs = set([]) - deferrednotifies = [] - machandlers = {} + newmacs.clear() + deferrednotifies.clear() + machandlers.clear() r = select.select((net4, net6), (), (), 60) if r: r = r[0] From 0afc3eb03a89b16511a142351c1654cea6ada8a4 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 5 Aug 2024 13:12:54 -0400 Subject: [PATCH 085/146] Port SSDP improvements to SLP It may not apply, but better to be consistent. --- .../confluent/discovery/protocols/slp.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/confluent_server/confluent/discovery/protocols/slp.py b/confluent_server/confluent/discovery/protocols/slp.py index 30acb475..f1e334f3 100644 --- a/confluent_server/confluent/discovery/protocols/slp.py +++ b/confluent_server/confluent/discovery/protocols/slp.py @@ -471,10 +471,13 @@ def snoop(handler, protocol=None): # socket in use can occur when aliased ipv4 are encountered net.bind(('', 427)) net4.bind(('', 427)) - + newmacs = set([]) + known_peers = set([]) + peerbymacaddress = {} + deferpeers = [] while True: try: - newmacs = set([]) + newmacs.clear() r, _, _ = select.select((net, net4), (), (), 60) # clear known_peers and peerbymacaddress # to avoid stale info getting in... @@ -482,9 +485,9 @@ def snoop(handler, protocol=None): # addresses that come close together # calling code needs to understand deeper context, as snoop # will now yield dupe info over time - known_peers = set([]) - peerbymacaddress = {} - deferpeers = [] + known_peers.clear() + peerbymacaddress.clear() + deferpeers.clear() while r and len(deferpeers) < 256: for s in r: (rsp, peer) = s.recvfrom(9000) From e07e6ed152ea1396902199c2cc72993b3ac88706 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 5 Aug 2024 14:56:23 -0400 Subject: [PATCH 086/146] Improve error handling in OpenBMC console --- confluent_server/confluent/plugins/console/openbmc.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/confluent_server/confluent/plugins/console/openbmc.py b/confluent_server/confluent/plugins/console/openbmc.py index 8677ce17..519ca2d4 100644 --- a/confluent_server/confluent/plugins/console/openbmc.py +++ b/confluent_server/confluent/plugins/console/openbmc.py @@ -134,7 +134,12 @@ class TsmConsole(conapi.Console): kv = util.TLSCertVerifier( self.nodeconfig, self.node, 'pubkeys.tls_hardwaremanager').verify_cert wc = webclient.SecureHTTPConnection(self.origbmc, 443, verifycallback=kv) - rsp = wc.grab_json_response_with_status('/login', {'data': [self.username.decode('utf8'), self.password.decode("utf8")]}, headers={'Content-Type': 'application/json'}) + try: + rsp = wc.grab_json_response_with_status('/login', {'data': [self.username.decode('utf8'), self.password.decode("utf8")]}, headers={'Content-Type': 'application/json', 'Accept': 'application/json'}) + except Exception as e: + raise cexc.TargetEndpointUnreachable(str(e)) + if rsp[1] > 400: + raise cexc.TargetEndpointBadCredentials bmc = self.bmc if '%' in self.bmc: prefix = self.bmc.split('%')[0] From 8c1381633116dc389554aa1518fe5adbda33d53a Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 5 Aug 2024 15:03:00 -0400 Subject: [PATCH 087/146] Fix fetch of model name for XCC3 systems --- confluent_server/confluent/discovery/handlers/xcc3.py | 1 + 1 file changed, 1 insertion(+) diff --git a/confluent_server/confluent/discovery/handlers/xcc3.py b/confluent_server/confluent/discovery/handlers/xcc3.py index 24974172..050186e9 100644 --- a/confluent_server/confluent/discovery/handlers/xcc3.py +++ b/confluent_server/confluent/discovery/handlers/xcc3.py @@ -33,6 +33,7 @@ class NodeHandler(redfishbmc.NodeHandler): ip, port = self.get_web_port_and_ip() c = webclient.SecureHTTPConnection(ip, port, verifycallback=self.validate_cert) + c.set_header('Accept', 'application/json') i = c.grab_json_response('/api/providers/logoninfo') modelname = i.get('items', [{}])[0].get('machine_name', None) if modelname: From feaef79060850b9cc3c0d682c3c4737f227f4c28 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 6 Aug 2024 09:30:13 -0400 Subject: [PATCH 088/146] Successfully track credential currency across change --- confluent_server/confluent/discovery/handlers/redfishbmc.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/confluent_server/confluent/discovery/handlers/redfishbmc.py b/confluent_server/confluent/discovery/handlers/redfishbmc.py index d4e164d6..7cf3f3d1 100644 --- a/confluent_server/confluent/discovery/handlers/redfishbmc.py +++ b/confluent_server/confluent/discovery/handlers/redfishbmc.py @@ -236,6 +236,8 @@ class NodeHandler(generic.NodeHandler): rsp, status = wc.grab_json_response_with_status(targaccturl, authupdate, method='PATCH') if status >= 300: raise Exception("Failed attempting to update credentials on BMC") + self.curruser = user + self.currpass = passwd wc.set_basic_credentials(user, passwd) _, status = wc.grab_json_response_with_status('/redfish/v1/Managers') tries = 10 From 21b1ac7690f301c9ef533868c8aae5e4a53dcf50 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 6 Aug 2024 09:34:46 -0400 Subject: [PATCH 089/146] Remove asyncore for jammy asyncore isn't needed before noble --- confluent_server/builddeb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/confluent_server/builddeb b/confluent_server/builddeb index 4071b5b1..a63d9d4f 100755 --- a/confluent_server/builddeb +++ b/confluent_server/builddeb @@ -35,6 +35,8 @@ cd deb_dist/!(*.orig)/ if [ "$OPKGNAME" = "confluent-server" ]; then if grep wheezy /etc/os-release; then sed -i 's/^\(Depends:.*\)/\1, python-confluent-client, python-lxml, python-eficompressor, python-pycryptodomex, python-dateutil, python-pyopenssl, python-msgpack/' debian/control + elif grep jammy /etc/os-release; then + sed -i 's/^\(Depends:.*\)/\1, confluent-client, python3-lxml, python3-eficompressor, python3-pycryptodome, python3-websocket, python3-msgpack, python3-eventlet, python3-pyparsing, python3-pyghmi, python3-paramiko, python3-pysnmp4, python3-libarchive-c, confluent-vtbufferd, python3-netifaces, python3-yaml, python3-dateutil/' debian/control else sed -i 's/^\(Depends:.*\)/\1, confluent-client, python3-lxml, python3-eficompressor, python3-pycryptodome, python3-websocket, python3-msgpack, python3-eventlet, python3-pyparsing, python3-pyghmi, python3-paramiko, python3-pysnmp4, python3-libarchive-c, confluent-vtbufferd, python3-netifaces, python3-yaml, python3-dateutil, python3-pyasyncore/' debian/control fi From ef1f51ef988ac296b06ccf20c6ea3078a5c13bfc Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 6 Aug 2024 10:05:39 -0400 Subject: [PATCH 090/146] Wire in bmc config clear to redfish --- .../confluent/plugins/hardwaremanagement/redfish.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/confluent_server/confluent/plugins/hardwaremanagement/redfish.py b/confluent_server/confluent/plugins/hardwaremanagement/redfish.py index f53cc393..2c2857de 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/redfish.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/redfish.py @@ -522,6 +522,8 @@ class IpmiHandler(object): return self.handle_sysconfig(True) elif self.element[1:3] == ['system', 'clear']: return self.handle_sysconfigclear() + elif self.element[1:3] == ['management_controller', 'clear']: + return self.handle_bmcconfigclear() elif self.element[1:3] == ['management_controller', 'licenses']: return self.handle_licenses() elif self.element[1:3] == ['management_controller', 'save_licenses']: @@ -1323,6 +1325,12 @@ class IpmiHandler(object): self.ipmicmd.set_bmc_configuration( self.inputdata.get_attributes(self.node)) + def handle_bmcconfigclear(self): + if 'read' == self.op: + raise exc.InvalidArgumentException( + 'Cannot read the "clear" resource') + self.ipmicmd.clear_bmc_configuration() + def handle_sysconfigclear(self): if 'read' == self.op: raise exc.InvalidArgumentException( From f2b9a4fa5d2bb5c5820c352b7fce641e79323c3d Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 6 Aug 2024 12:25:21 -0400 Subject: [PATCH 091/146] Improve handling of ssh service being pre-hooked --- imgutil/imgutil | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/imgutil/imgutil b/imgutil/imgutil index bc34af01..5b5de0b2 100644 --- a/imgutil/imgutil +++ b/imgutil/imgutil @@ -661,11 +661,20 @@ class DebHandler(OsHandler): run_constrainedx(fancy_chroot, (args, self.targpath)) args.cmd = ['apt-get', '-y', 'install'] + self.includepkgs run_constrainedx(fancy_chroot, (args, self.targpath)) - servicefile = os.path.join(self.targpath, 'usr/lib/systemd/system/ssh.service') + servicefile = os.path.join( + self.targpath, 'usr/lib/systemd/system/ssh.service') if os.path.exists(servicefile): - os.symlink('/usr/lib/systemd/system/ssh.service', os.path.join(self.targpath, 'etc/systemd/system/multi-user.target.wants/ssh.service')) + targfile = os.path.join( + self.targpath, + 'etc/systemd/system/multi-user.target.wants/ssh.service') + if not os.path.exists(targfile): + os.symlink('/usr/lib/systemd/system/ssh.service', targfile) else: - os.symlink('/usr/lib/systemd/system/sshd.service', os.path.join(self.targpath, 'etc/systemd/system/multi-user.target.wants/sshd.service')) + targfile = os.path.join( + self.targpath, + 'etc/systemd/system/multi-user.target.wants/sshd.service') + if not os.path.exists(targfile): + os.symlink('/usr/lib/systemd/system/sshd.service', targfile) From 7ab76004925ff82cceafaaaac158d71f9ba0f04b Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 7 Aug 2024 07:56:11 -0400 Subject: [PATCH 092/146] Add cpio dependency for imgutil --- imgutil/confluent_imgutil.spec.tmpl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/imgutil/confluent_imgutil.spec.tmpl b/imgutil/confluent_imgutil.spec.tmpl index 35ed4070..f7dea7a7 100644 --- a/imgutil/confluent_imgutil.spec.tmpl +++ b/imgutil/confluent_imgutil.spec.tmpl @@ -8,13 +8,13 @@ Source: confluent_imgutil.tar.xz BuildArch: noarch BuildRoot: /tmp/ %if "%{dist}" == ".el8" -Requires: squashfs-tools +Requires: squashfs-tools cpio %else %if "%{dist}" == ".el9" -Requires: squashfs-tools +Requires: squashfs-tools cpio %else %if "%{dist}" == ".el7" -Requires: squashfs-tools +Requires: squashfs-tools cpio %else Requires: squashfs %endif From 187fda4bb865b0f11c6333f6145e4a02f043527e Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 7 Aug 2024 07:58:08 -0400 Subject: [PATCH 093/146] Add debootstrap dependency for imgutil --- imgutil/control.tmpl | 1 + 1 file changed, 1 insertion(+) diff --git a/imgutil/control.tmpl b/imgutil/control.tmpl index a0fe21af..3bc8644c 100644 --- a/imgutil/control.tmpl +++ b/imgutil/control.tmpl @@ -5,4 +5,5 @@ Priority: optional Maintainer: Jarrod Johnson Description: Web frontend for confluent server Architecture: all +Depends: debootstrap From ca4955101d3ca912bf2c030ba77285212a2e149d Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 7 Aug 2024 08:40:10 -0400 Subject: [PATCH 094/146] Improve "realness" of imgutil exec context Utilities that expected /dev/pts will now be satisfied, as a new /dev/pts is mounted. Further, systemd added a check in various utilities that was fouled by the previous method of appearing to have a root filesystem. Before, after chroot, we would bind mount / to itself, and this made things using /proc/mounts, /proc/self/mountinfo, df, mount, etc happy that there is a real looking root filesystem. However, by doing it after the chroot, systemd could statx on '..' and get a different mnt id than /. So it had to be done prior to the chroot. However it also had to be done before other mounts as bind mounting over it would block the submounts. This more closely imitates the initramfs behavior, where '/' starts life as a 'real' filesystem before being mounted up and switched into. This behavior was made to imitate the 'start_root.c' behavior as that seems to be more broadly successful. --- imgutil/imgutil | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/imgutil/imgutil b/imgutil/imgutil index 5b5de0b2..c5446069 100644 --- a/imgutil/imgutil +++ b/imgutil/imgutil @@ -963,7 +963,6 @@ def fancy_chroot(args, installroot): _mount('none', dstresolv, flags=MS_RDONLY|MS_REMOUNT|MS_BIND) os.chroot(installroot) os.chdir('/') - _mount('/', '/', flags=MS_BIND) # Make / manifest as a mounted filesystem in exec os.environ['PS1'] = '[\x1b[1m\x1b[4mIMGUTIL EXEC {0}\x1b[0m \\W]$ '.format(imgname) os.environ['CONFLUENT_IMGUTIL_MODE'] = 'exec' if oshandler: @@ -1004,7 +1003,13 @@ def build_root_backend(optargs): def _mount_constrained_fs(args, installroot): + # This is prepping for a chroot. + # For the target environment to be content with having a root + # filesystem, installroot must be a 'mount' entry of it's own, + # so bind mount to itself to satisfy + _mount(installroot, installroot, flags=MS_BIND) _mount('/dev', os.path.join(installroot, 'dev'), flags=MS_BIND|MS_RDONLY) + _mount('/dev/pts', os.path.join(installroot, 'dev/pts'), flags=MS_BIND|MS_RDONLY) _mount('proc', os.path.join(installroot, 'proc'), fstype='proc') _mount('sys', os.path.join(installroot, 'sys'), fstype='sysfs') _mount('runfs', os.path.join(installroot, 'run'), fstype='tmpfs') From 4453ba3b64bb41e6e37ae204115fdfdf6d4bc296 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 7 Aug 2024 09:20:34 -0400 Subject: [PATCH 095/146] Add cpio to confluent_server In order to do osdeploy processing, we must have cpio --- confluent_server/confluent_server.spec.tmpl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/confluent_server/confluent_server.spec.tmpl b/confluent_server/confluent_server.spec.tmpl index bf81c969..04e63b21 100644 --- a/confluent_server/confluent_server.spec.tmpl +++ b/confluent_server/confluent_server.spec.tmpl @@ -14,13 +14,13 @@ Prefix: %{_prefix} BuildArch: noarch Requires: confluent_vtbufferd %if "%{dist}" == ".el7" -Requires: python-pyghmi >= 1.0.34, python-eventlet, python-greenlet, python-pycryptodomex >= 3.4.7, confluent_client == %{version}, python-pyparsing, python-paramiko, python-dnspython, python-netifaces, python2-pyasn1 >= 0.2.3, python-pysnmp >= 4.3.4, python-lxml, python-eficompressor, python-setuptools, python-dateutil, python-websocket-client python2-msgpack python-libarchive-c python-yaml python-monotonic +Requires: python-pyghmi >= 1.0.34, python-eventlet, python-greenlet, python-pycryptodomex >= 3.4.7, confluent_client == %{version}, python-pyparsing, python-paramiko, python-dnspython, python-netifaces, python2-pyasn1 >= 0.2.3, python-pysnmp >= 4.3.4, python-lxml, python-eficompressor, python-setuptools, python-dateutil, python-websocket-client python2-msgpack python-libarchive-c python-yaml python-monotonic cpio %else %if "%{dist}" == ".el8" -Requires: python3-pyghmi >= 1.0.34, python3-eventlet, python3-greenlet, python3-pycryptodomex >= 3.4.7, confluent_client == %{version}, python3-pyparsing, python3-paramiko, python3-dns, python3-netifaces, python3-pyasn1 >= 0.2.3, python3-pysnmp >= 4.3.4, python3-lxml, python3-eficompressor, python3-setuptools, python3-dateutil, python3-enum34, python3-asn1crypto, python3-cffi, python3-pyOpenSSL, python3-websocket-client python3-msgpack python3-libarchive-c python3-yaml openssl iproute +Requires: python3-pyghmi >= 1.0.34, python3-eventlet, python3-greenlet, python3-pycryptodomex >= 3.4.7, confluent_client == %{version}, python3-pyparsing, python3-paramiko, python3-dns, python3-netifaces, python3-pyasn1 >= 0.2.3, python3-pysnmp >= 4.3.4, python3-lxml, python3-eficompressor, python3-setuptools, python3-dateutil, python3-enum34, python3-asn1crypto, python3-cffi, python3-pyOpenSSL, python3-websocket-client python3-msgpack python3-libarchive-c python3-yaml openssl iproute cpio %else %if "%{dist}" == ".el9" -Requires: python3-pyghmi >= 1.0.34, python3-eventlet, python3-greenlet, python3-pycryptodomex >= 3.4.7, confluent_client == %{version}, python3-pyparsing, python3-paramiko, python3-dns, python3-netifaces, python3-pyasn1 >= 0.2.3, python3-pysnmp >= 4.3.4, python3-lxml, python3-eficompressor, python3-setuptools, python3-dateutil, python3-cffi, python3-pyOpenSSL, python3-websocket-client python3-msgpack python3-libarchive-c python3-yaml openssl iproute +Requires: python3-pyghmi >= 1.0.34, python3-eventlet, python3-greenlet, python3-pycryptodomex >= 3.4.7, confluent_client == %{version}, python3-pyparsing, python3-paramiko, python3-dns, python3-netifaces, python3-pyasn1 >= 0.2.3, python3-pysnmp >= 4.3.4, python3-lxml, python3-eficompressor, python3-setuptools, python3-dateutil, python3-cffi, python3-pyOpenSSL, python3-websocket-client python3-msgpack python3-libarchive-c python3-yaml openssl iproute cpio %else Requires: python3-dbm,python3-pyghmi >= 1.0.34, python3-eventlet, python3-greenlet, python3-pycryptodome >= 3.4.7, confluent_client == %{version}, python3-pyparsing, python3-paramiko, python3-dnspython, python3-netifaces, python3-pyasn1 >= 0.2.3, python3-pysnmp >= 4.3.4, python3-lxml, python3-eficompressor, python3-setuptools, python3-dateutil, python3-cffi, python3-pyOpenSSL, python3-websocket-client python3-msgpack python3-libarchive-c python3-PyYAML openssl iproute %endif From 2fc4483bba0316c09931268b4daa19ba5e8e2042 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 8 Aug 2024 17:09:33 -0400 Subject: [PATCH 096/146] Backport SLP performance enhancement from async branch Same concept that could bog down async variant could be a slowdown for normal confluent. --- confluent_server/confluent/discovery/protocols/slp.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/confluent_server/confluent/discovery/protocols/slp.py b/confluent_server/confluent/discovery/protocols/slp.py index f1e334f3..3ca7cd01 100644 --- a/confluent_server/confluent/discovery/protocols/slp.py +++ b/confluent_server/confluent/discovery/protocols/slp.py @@ -493,6 +493,8 @@ def snoop(handler, protocol=None): (rsp, peer) = s.recvfrom(9000) if peer in known_peers: continue + if peer in deferpeers: + continue mac = neighutil.get_hwaddr(peer[0]) if not mac: probepeer = (peer[0], struct.unpack('H', os.urandom(2))[0] | 1025) + peer[2:] From 8fd39c36bb55b94f6fed48fb1163d4afdf713661 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 9 Aug 2024 07:55:42 -0400 Subject: [PATCH 097/146] Fix some mistakes in confignet --- confluent_osdeploy/common/profile/scripts/confignet | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/confluent_osdeploy/common/profile/scripts/confignet b/confluent_osdeploy/common/profile/scripts/confignet index 72462834..cb5684a8 100644 --- a/confluent_osdeploy/common/profile/scripts/confignet +++ b/confluent_osdeploy/common/profile/scripts/confignet @@ -86,7 +86,7 @@ def map_idx_to_name(): for line in subprocess.check_output(['ip', 'l']).decode('utf8').splitlines(): if line.startswith(' ') and 'link/' in line: typ = line.split()[0].split('/')[1] - devtype[prevdev] = typ if type != 'ether' else 'ethernet' + devtype[prevdev] = typ if typ != 'ether' else 'ethernet' if line.startswith(' '): continue idx, iface, rst = line.split(':', 2) @@ -413,7 +413,8 @@ class NetworkManager(object): cargs.append(arg) cargs.append(cmdargs[arg]) if u: - cmdargs['connection.interface-name'] = iname + cargs.append('connection.interface-name') + cargs.append(iname) subprocess.check_call(['nmcli', 'c', 'm', u] + cargs) subprocess.check_call(['nmcli', 'c', 'u', u]) else: From 6833cd9c5301ebfca28c3c68ee2f912b2ee0d643 Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Fri, 9 Aug 2024 17:58:48 +0200 Subject: [PATCH 098/146] Add VLAN/PKEY support to confignet Introduce new node attribute net.vlan_id to support VLAN/PKEY configuration using confignet. --- .../common/profile/scripts/confignet | 27 ++++++++++++++----- .../confluent/config/attributes.py | 8 ++++-- confluent_server/confluent/netutil.py | 3 +++ 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/confluent_osdeploy/common/profile/scripts/confignet b/confluent_osdeploy/common/profile/scripts/confignet index cb5684a8..7b0eddf9 100644 --- a/confluent_osdeploy/common/profile/scripts/confignet +++ b/confluent_osdeploy/common/profile/scripts/confignet @@ -405,20 +405,35 @@ class NetworkManager(object): else: cname = stgs.get('connection_name', None) iname = list(cfg['interfaces'])[0] - if not cname: - cname = iname + ctype = self.devtypes[iname] + if stgs.get('vlan_id', None): + vlan = stgs['vlan_id'] + if ctype == 'infiniband': + vlan = '0x{0}'.format(vlan) if not vlan.startswith('0x') else vlan + cmdargs['infiniband.parent'] = iname + cmdargs['infiniband.p-key'] = vlan + iname = '{0}.{1}'.format(iname, vlan[2:]) + cname = iname if not cname else cname + elif ctype == 'ethernet': + ctype = 'vlan' + cmdargs['vlan.parent'] = iname + cmdargs['vlan.id'] = vlan + iname = '{0}.{1}'.format(iname, vlan) + cname = iname if not cname else cname + else: + sys.stderr.write("Warning, unknown interface_name ({0}) device type ({1}) for VLAN/PKEY, skipping setup\n".format(iname, ctype)) + return + cname = iname if not cname else cname u = self.uuidbyname.get(cname, None) cargs = [] for arg in cmdargs: cargs.append(arg) cargs.append(cmdargs[arg]) if u: - cargs.append('connection.interface-name') - cargs.append(iname) - subprocess.check_call(['nmcli', 'c', 'm', u] + cargs) + subprocess.check_call(['nmcli', 'c', 'm', u, 'connection.interface-name', iname] + cargs) subprocess.check_call(['nmcli', 'c', 'u', u]) else: - subprocess.check_call(['nmcli', 'c', 'add', 'type', self.devtypes[iname], 'con-name', cname, 'connection.interface-name', iname] + cargs) + subprocess.check_call(['nmcli', 'c', 'add', 'type', ctype, 'con-name', cname, 'connection.interface-name', iname] + cargs) self.read_connections() u = self.uuidbyname.get(cname, None) if u: diff --git a/confluent_server/confluent/config/attributes.py b/confluent_server/confluent/config/attributes.py index 101ee03d..f926c962 100644 --- a/confluent_server/confluent/config/attributes.py +++ b/confluent_server/confluent/config/attributes.py @@ -469,9 +469,13 @@ node = { 'net.interface_names': { 'description': 'Interface name or comma delimited list of names to match for this interface. It is generally recommended ' 'to leave this blank unless needing to set up interfaces that are not on a common subnet with a confluent server, ' - 'as confluent servers provide autodetection for matching the correct network definition to an interface.' + 'as confluent servers provide autodetection for matching the correct network definition to an interface. ' 'This would be the default name per the deployed OS and can be a comma delimited list to denote members of ' - 'a team' + 'a team or a single interface for VLAN/PKEY connections.' + }, + 'net.vlan_id': { + 'description': 'Ethernet VLAN or InfiniBand PKEY to use for this connection. ' + 'Specify the parent device using net.interface_names.' }, 'net.ipv4_address': { 'description': 'When configuring static, use this address. If ' diff --git a/confluent_server/confluent/netutil.py b/confluent_server/confluent/netutil.py index 9bac92c2..c1a9210a 100644 --- a/confluent_server/confluent/netutil.py +++ b/confluent_server/confluent/netutil.py @@ -193,6 +193,9 @@ class NetManager(object): iname = attribs.get('interface_names', None) if iname: myattribs['interface_names'] = iname + vlanid = attribs.get('vlan_id', None) + if vlanid: + myattribs['vlan_id'] = vlanid teammod = attribs.get('team_mode', None) if teammod: myattribs['team_mode'] = teammod From 6943c2dc0f1f0cfa7b530d6d09ccbdd199764efb Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Fri, 9 Aug 2024 19:38:45 +0200 Subject: [PATCH 099/146] Make sure VLAN/PKEY connections are created last Needed for VLANs on bond connections etc. --- confluent_osdeploy/common/profile/scripts/confignet | 2 ++ 1 file changed, 2 insertions(+) diff --git a/confluent_osdeploy/common/profile/scripts/confignet b/confluent_osdeploy/common/profile/scripts/confignet index 7b0eddf9..20fcc8b8 100644 --- a/confluent_osdeploy/common/profile/scripts/confignet +++ b/confluent_osdeploy/common/profile/scripts/confignet @@ -516,6 +516,8 @@ if __name__ == '__main__': netname_to_interfaces['default']['interfaces'] -= netname_to_interfaces[netn]['interfaces'] if not netname_to_interfaces['default']['interfaces']: del netname_to_interfaces['default'] + # Make sure VLAN/PKEY connections are created last + netname_to_interfaces = dict(sorted(netname_to_interfaces.items(), key=lambda item: 'vlan_id' in item[1]['settings'])) rm_tmp_llas(tmpllas) if os.path.exists('/usr/sbin/netplan'): nm = NetplanManager(dc) From 005adec437dc631d5b3f9f7b38cc640336bdc636 Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Fri, 9 Aug 2024 19:45:19 +0200 Subject: [PATCH 100/146] Add error handling for interface_names --- confluent_osdeploy/common/profile/scripts/confignet | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/confluent_osdeploy/common/profile/scripts/confignet b/confluent_osdeploy/common/profile/scripts/confignet index 20fcc8b8..562a8ca1 100644 --- a/confluent_osdeploy/common/profile/scripts/confignet +++ b/confluent_osdeploy/common/profile/scripts/confignet @@ -405,7 +405,10 @@ class NetworkManager(object): else: cname = stgs.get('connection_name', None) iname = list(cfg['interfaces'])[0] - ctype = self.devtypes[iname] + ctype = self.devtypes.get(iname, None) + if not ctype: + sys.stderr.write("Warning, no device found for interface_name ({0}), skipping setup\n".format(iname)) + return if stgs.get('vlan_id', None): vlan = stgs['vlan_id'] if ctype == 'infiniband': From 09611744258586eba22cb5fa19e16d5b6ca2759b Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Fri, 9 Aug 2024 19:55:42 +0200 Subject: [PATCH 101/146] Remove redundant code --- confluent_osdeploy/common/profile/scripts/confignet | 2 -- 1 file changed, 2 deletions(-) diff --git a/confluent_osdeploy/common/profile/scripts/confignet b/confluent_osdeploy/common/profile/scripts/confignet index 562a8ca1..650f4eb6 100644 --- a/confluent_osdeploy/common/profile/scripts/confignet +++ b/confluent_osdeploy/common/profile/scripts/confignet @@ -416,13 +416,11 @@ class NetworkManager(object): cmdargs['infiniband.parent'] = iname cmdargs['infiniband.p-key'] = vlan iname = '{0}.{1}'.format(iname, vlan[2:]) - cname = iname if not cname else cname elif ctype == 'ethernet': ctype = 'vlan' cmdargs['vlan.parent'] = iname cmdargs['vlan.id'] = vlan iname = '{0}.{1}'.format(iname, vlan) - cname = iname if not cname else cname else: sys.stderr.write("Warning, unknown interface_name ({0}) device type ({1}) for VLAN/PKEY, skipping setup\n".format(iname, ctype)) return From a6a1907611411f19092a7581fed1d5d15415cff9 Mon Sep 17 00:00:00 2001 From: Adrian Reber Date: Tue, 13 Aug 2024 17:30:43 +0200 Subject: [PATCH 102/146] Do not overwrite the node SSH key with the last found public key Instead of overwriting the SSH public code for the node concatenate all found SSH keys together in one file. Signed-off-by: Adrian Reber --- confluent_server/confluent/sshutil.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/confluent_server/confluent/sshutil.py b/confluent_server/confluent/sshutil.py index cf17f37a..1f8960d8 100644 --- a/confluent_server/confluent/sshutil.py +++ b/confluent_server/confluent/sshutil.py @@ -213,15 +213,17 @@ def initialize_root_key(generate, automation=False): suffix = 'automationpubkey' else: suffix = 'rootpubkey' + keyname = '/var/lib/confluent/public/site/ssh/{0}.{1}'.format( + myname, suffix) for auth in authorized: - shutil.copy( - auth, - '/var/lib/confluent/public/site/ssh/{0}.{1}'.format( - myname, suffix)) - os.chmod('/var/lib/confluent/public/site/ssh/{0}.{1}'.format( - myname, suffix), 0o644) - os.chown('/var/lib/confluent/public/site/ssh/{0}.{1}'.format( - myname, suffix), neededuid, -1) + local_key = open(auth, 'r') + dest = open(keyname, 'a') + dest.write(local_key.read()) + local_key.close() + dest.close() + if os.path.exists(keyname): + os.chmod(keyname, 0o644) + os.chown(keyname, neededuid, -1) if alreadyexist: raise AlreadyExists() From 29d0e904876a249a6a3afdddc789b931b589e66e Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 14 Aug 2024 11:26:51 -0400 Subject: [PATCH 103/146] Implement confluentdbutil 'merge' For now, implement 'skip', where conflicting nodes/groups are ignored in new input. --- confluent_server/bin/confluentdbutil | 15 ++- .../confluent/config/configmanager.py | 106 ++++++++++++------ 2 files changed, 79 insertions(+), 42 deletions(-) diff --git a/confluent_server/bin/confluentdbutil b/confluent_server/bin/confluentdbutil index 25a5acf8..b7c1e5c7 100755 --- a/confluent_server/bin/confluentdbutil +++ b/confluent_server/bin/confluentdbutil @@ -30,7 +30,7 @@ import confluent.config.conf as conf import confluent.main as main argparser = optparse.OptionParser( - usage="Usage: %prog [options] [dump|restore] [path]") + usage="Usage: %prog [options] [dump|restore|merge] [path]") argparser.add_option('-p', '--password', help='Password to use to protect/unlock a protected dump') argparser.add_option('-i', '--interactivepassword', help='Prompt for password', @@ -51,13 +51,13 @@ argparser.add_option('-s', '--skipkeys', action='store_true', 'data is needed. keys do not change and as such ' 'they do not require incremental backup') (options, args) = argparser.parse_args() -if len(args) != 2 or args[0] not in ('dump', 'restore'): +if len(args) != 2 or args[0] not in ('dump', 'restore', 'merge'): argparser.print_help() sys.exit(1) dumpdir = args[1] -if args[0] == 'restore': +if args[0] in ('restore', 'merge'): pid = main.is_running() if pid is not None: print("Confluent is running, must shut down to restore db") @@ -69,9 +69,12 @@ if args[0] == 'restore': if options.interactivepassword: password = getpass.getpass('Enter password to restore backup: ') try: - cfm.init(True) - cfm.statelessmode = True - cfm.restore_db_from_directory(dumpdir, password) + stateless = args[0] == 'restore' + cfm.init(stateless) + cfm.statelessmode = stateless + cfm.restore_db_from_directory( + dumpdir, password, + merge="skip" if args[0] == 'merge' else False) cfm.statelessmode = False cfm.ConfigManager.wait_for_sync(True) if owner != 0: diff --git a/confluent_server/confluent/config/configmanager.py b/confluent_server/confluent/config/configmanager.py index 6cbf4604..788c2d60 100644 --- a/confluent_server/confluent/config/configmanager.py +++ b/confluent_server/confluent/config/configmanager.py @@ -1903,7 +1903,7 @@ class ConfigManager(object): def add_group_attributes(self, attribmap): self.set_group_attributes(attribmap, autocreate=True) - def set_group_attributes(self, attribmap, autocreate=False): + def set_group_attributes(self, attribmap, autocreate=False, merge="replace", keydata=None, skipped=None): for group in attribmap: curr = attribmap[group] for attrib in curr: @@ -1924,11 +1924,11 @@ class ConfigManager(object): if cfgstreams: exec_on_followers('_rpc_set_group_attributes', self.tenant, attribmap, autocreate) - self._true_set_group_attributes(attribmap, autocreate) + self._true_set_group_attributes(attribmap, autocreate, merge=merge, keydata=keydata, skipped=skipped) - def _true_set_group_attributes(self, attribmap, autocreate=False): + def _true_set_group_attributes(self, attribmap, autocreate=False, merge="replace", keydata=None, skipped=None): changeset = {} - for group in attribmap: + for group in list(attribmap): if group == '': raise ValueError('"{0}" is not a valid group name'.format( group)) @@ -1941,6 +1941,11 @@ class ConfigManager(object): group)) if not autocreate and group not in self._cfgstore['nodegroups']: raise ValueError("{0} group does not exist".format(group)) + if merge == 'skip' and group in self._cfgstore['nodegroups']: + if skipped is not None: + skipped.append(group) + del attribmap[group] + continue for attr in list(attribmap[group]): # first do a pass to normalize out any aliased attribute names if attr in _attraliases: @@ -2015,6 +2020,9 @@ class ConfigManager(object): newdict = {'value': attribmap[group][attr]} else: newdict = attribmap[group][attr] + if keydata and attr.startswith('secret.') and 'cryptvalue' in newdict: + newdict['value'] = decrypt_value(newdict['cryptvalue'], keydata['cryptkey'], keydata['integritykey']) + del newdict['cryptvalue'] if 'value' in newdict and attr.startswith("secret."): newdict['cryptvalue'] = crypt_value(newdict['value']) del newdict['value'] @@ -2349,7 +2357,7 @@ class ConfigManager(object): - def set_node_attributes(self, attribmap, autocreate=False): + def set_node_attributes(self, attribmap, autocreate=False, merge="replace", keydata=None, skipped=None): for node in attribmap: curr = attribmap[node] for attrib in curr: @@ -2370,9 +2378,9 @@ class ConfigManager(object): if cfgstreams: exec_on_followers('_rpc_set_node_attributes', self.tenant, attribmap, autocreate) - self._true_set_node_attributes(attribmap, autocreate) + self._true_set_node_attributes(attribmap, autocreate, merge, keydata, skipped) - def _true_set_node_attributes(self, attribmap, autocreate): + def _true_set_node_attributes(self, attribmap, autocreate, merge="replace", keydata=None, skipped=None): # TODO(jbjohnso): multi mgr support, here if we have peers, # pickle the arguments and fire them off in eventlet # flows to peers, all should have the same result @@ -2380,7 +2388,7 @@ class ConfigManager(object): changeset = {} # first do a sanity check of the input upfront # this mitigates risk of arguments being partially applied - for node in attribmap: + for node in list(attribmap): node = confluent.util.stringify(node) if node == '': raise ValueError('"{0}" is not a valid node name'.format(node)) @@ -2393,6 +2401,11 @@ class ConfigManager(object): '"{0}" is not a valid node name'.format(node)) if autocreate is False and node not in self._cfgstore['nodes']: raise ValueError("node {0} does not exist".format(node)) + if merge == "skip" and node in self._cfgstore['nodes']: + del attribmap[node] + if skipped is not None: + skipped.append(node) + continue if 'groups' not in attribmap[node] and node not in self._cfgstore['nodes']: attribmap[node]['groups'] = [] for attrname in list(attribmap[node]): @@ -2463,6 +2476,9 @@ class ConfigManager(object): # add check here, skip None attributes if newdict is None: continue + if keydata and attrname.startswith('secret.') and 'cryptvalue' in newdict: + newdict['value'] = decrypt_value(newdict['cryptvalue'], keydata['cryptkey'], keydata['integritykey']) + del newdict['cryptvalue'] if 'value' in newdict and attrname.startswith("secret."): newdict['cryptvalue'] = crypt_value(newdict['value']) del newdict['value'] @@ -2503,14 +2519,14 @@ class ConfigManager(object): self._bg_sync_to_file() #TODO: wait for synchronization to suceed/fail??) - def _load_from_json(self, jsondata, sync=True): + def _load_from_json(self, jsondata, sync=True, merge=False, keydata=None): self.inrestore = True try: - self._load_from_json_backend(jsondata, sync=True) + self._load_from_json_backend(jsondata, sync=True, merge=merge, keydata=keydata) finally: self.inrestore = False - def _load_from_json_backend(self, jsondata, sync=True): + def _load_from_json_backend(self, jsondata, sync=True, merge=False, keydata=None): """Load fresh configuration data from jsondata :param jsondata: String of jsondata @@ -2563,20 +2579,27 @@ class ConfigManager(object): pass # Now we have to iterate through each fixed up element, using the # set attribute to flesh out inheritence and expressions - _cfgstore['main']['idmap'] = {} + if (not merge) or _cfgstore.get('main', {}).get('idmap', None) is None: + _cfgstore['main']['idmap'] = {} + attribmerge = merge if merge else "replace" for confarea in _config_areas: - self._cfgstore[confarea] = {} + if not merge or confarea not in self._cfgstore: + self._cfgstore[confarea] = {} if confarea not in tmpconfig: continue if confarea == 'nodes': - self.set_node_attributes(tmpconfig[confarea], True) + self.set_node_attributes(tmpconfig[confarea], True, merge=attribmerge, keydata=keydata) elif confarea == 'nodegroups': - self.set_group_attributes(tmpconfig[confarea], True) + self.set_group_attributes(tmpconfig[confarea], True, merge=attribmerge, keydata=keydata) elif confarea == 'usergroups': + if merge: + continue for usergroup in tmpconfig[confarea]: role = tmpconfig[confarea][usergroup].get('role', 'Administrator') self.create_usergroup(usergroup, role=role) elif confarea == 'users': + if merge: + continue for user in tmpconfig[confarea]: ucfg = tmpconfig[confarea][user] uid = ucfg.get('id', None) @@ -2876,7 +2899,7 @@ def _restore_keys(jsond, password, newpassword=None, sync=True): newpassword = keyfile.read() set_global('master_privacy_key', _format_key(cryptkey, password=newpassword), sync) - if integritykey: + if integritykey: set_global('master_integrity_key', _format_key(integritykey, password=newpassword), sync) _masterkey = cryptkey @@ -2911,35 +2934,46 @@ def _dump_keys(password, dojson=True): return keydata -def restore_db_from_directory(location, password): +def restore_db_from_directory(location, password, merge=False): + kdd = None try: with open(os.path.join(location, 'keys.json'), 'r') as cfgfile: keydata = cfgfile.read() - json.loads(keydata) - _restore_keys(keydata, password) + kdd = json.loads(keydata) + if merge: + if 'cryptkey' in kdd: + kdd['cryptkey'] = _parse_key(kdd['cryptkey'], password) + if 'integritykey' in kdd: + kdd['integritykey'] = _parse_key(kdd['integritykey'], password) + else: + kdd['integritykey'] = None # GCM + else: + kdd = None + _restore_keys(keydata, password) except IOError as e: if e.errno == 2: raise Exception("Cannot restore without keys, this may be a " "redacted dump") - try: - moreglobals = json.load(open(os.path.join(location, 'globals.json'))) - for globvar in moreglobals: - set_global(globvar, moreglobals[globvar]) - except IOError as e: - if e.errno != 2: - raise - try: - collective = json.load(open(os.path.join(location, 'collective.json'))) - _cfgstore['collective'] = {} - for coll in collective: - add_collective_member(coll, collective[coll]['address'], - collective[coll]['fingerprint']) - except IOError as e: - if e.errno != 2: - raise + if not merge: + try: + moreglobals = json.load(open(os.path.join(location, 'globals.json'))) + for globvar in moreglobals: + set_global(globvar, moreglobals[globvar]) + except IOError as e: + if e.errno != 2: + raise + try: + collective = json.load(open(os.path.join(location, 'collective.json'))) + _cfgstore['collective'] = {} + for coll in collective: + add_collective_member(coll, collective[coll]['address'], + collective[coll]['fingerprint']) + except IOError as e: + if e.errno != 2: + raise with open(os.path.join(location, 'main.json'), 'r') as cfgfile: cfgdata = cfgfile.read() - ConfigManager(tenant=None)._load_from_json(cfgdata) + ConfigManager(tenant=None)._load_from_json(cfgdata, merge=merge, keydata=kdd) ConfigManager.wait_for_sync(True) From 28b88bdb12d78f16a29549a3ab4f2d914252c434 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 14 Aug 2024 11:40:11 -0400 Subject: [PATCH 104/146] Add reporting of skipped nodes in a 'skip' merge --- confluent_server/bin/confluentdbutil | 16 +++++++++++++--- .../confluent/config/configmanager.py | 16 +++++++++------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/confluent_server/bin/confluentdbutil b/confluent_server/bin/confluentdbutil index b7c1e5c7..e74c2ab4 100755 --- a/confluent_server/bin/confluentdbutil +++ b/confluent_server/bin/confluentdbutil @@ -1,7 +1,7 @@ -#!/usr/bin/python2 +#!/usr/bin/python3 # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2017 Lenovo +# Copyright 2017,2024 Lenovo # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -72,9 +72,19 @@ if args[0] in ('restore', 'merge'): stateless = args[0] == 'restore' cfm.init(stateless) cfm.statelessmode = stateless + skipped = {'nodes': [], 'nodegroups': []} cfm.restore_db_from_directory( dumpdir, password, - merge="skip" if args[0] == 'merge' else False) + merge="skip" if args[0] == 'merge' else False, skipped=skipped) + if skipped['nodes']: + skippedn = ','.join(skipped['nodes']) + print('The following nodes were skipped during merge: ' + '{}'.format(skippedn)) + if skipped['nodegroups']: + skippedn = ','.join(skipped['nodegroups']) + print('The following node groups were skipped during merge: ' + '{}'.format(skippedn)) + cfm.statelessmode = False cfm.ConfigManager.wait_for_sync(True) if owner != 0: diff --git a/confluent_server/confluent/config/configmanager.py b/confluent_server/confluent/config/configmanager.py index 788c2d60..7702b97d 100644 --- a/confluent_server/confluent/config/configmanager.py +++ b/confluent_server/confluent/config/configmanager.py @@ -2519,19 +2519,21 @@ class ConfigManager(object): self._bg_sync_to_file() #TODO: wait for synchronization to suceed/fail??) - def _load_from_json(self, jsondata, sync=True, merge=False, keydata=None): + def _load_from_json(self, jsondata, sync=True, merge=False, keydata=None, skipped=None): self.inrestore = True try: - self._load_from_json_backend(jsondata, sync=True, merge=merge, keydata=keydata) + self._load_from_json_backend(jsondata, sync=True, merge=merge, keydata=keydata, skipped=skipped) finally: self.inrestore = False - def _load_from_json_backend(self, jsondata, sync=True, merge=False, keydata=None): + def _load_from_json_backend(self, jsondata, sync=True, merge=False, keydata=None, skipped=None): """Load fresh configuration data from jsondata :param jsondata: String of jsondata :return: """ + if not skipped: + skipped = {'nodes': None, 'nodegroups': None} dumpdata = json.loads(jsondata) tmpconfig = {} for confarea in _config_areas: @@ -2588,9 +2590,9 @@ class ConfigManager(object): if confarea not in tmpconfig: continue if confarea == 'nodes': - self.set_node_attributes(tmpconfig[confarea], True, merge=attribmerge, keydata=keydata) + self.set_node_attributes(tmpconfig[confarea], True, merge=attribmerge, keydata=keydata, skipped=skipped['nodes']) elif confarea == 'nodegroups': - self.set_group_attributes(tmpconfig[confarea], True, merge=attribmerge, keydata=keydata) + self.set_group_attributes(tmpconfig[confarea], True, merge=attribmerge, keydata=keydata, skipped=skipped['nodegroups']) elif confarea == 'usergroups': if merge: continue @@ -2934,7 +2936,7 @@ def _dump_keys(password, dojson=True): return keydata -def restore_db_from_directory(location, password, merge=False): +def restore_db_from_directory(location, password, merge=False, skipped=None): kdd = None try: with open(os.path.join(location, 'keys.json'), 'r') as cfgfile: @@ -2973,7 +2975,7 @@ def restore_db_from_directory(location, password, merge=False): raise with open(os.path.join(location, 'main.json'), 'r') as cfgfile: cfgdata = cfgfile.read() - ConfigManager(tenant=None)._load_from_json(cfgdata, merge=merge, keydata=kdd) + ConfigManager(tenant=None)._load_from_json(cfgdata, merge=merge, keydata=kdd, skipped=skipped) ConfigManager.wait_for_sync(True) From 82e0d9c434482688b9278178cf741db69ced07bd Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 14 Aug 2024 16:08:02 -0400 Subject: [PATCH 105/146] Rework ssh key init to reset key and use context management --- confluent_server/confluent/sshutil.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/confluent_server/confluent/sshutil.py b/confluent_server/confluent/sshutil.py index 1f8960d8..40512648 100644 --- a/confluent_server/confluent/sshutil.py +++ b/confluent_server/confluent/sshutil.py @@ -215,12 +215,13 @@ def initialize_root_key(generate, automation=False): suffix = 'rootpubkey' keyname = '/var/lib/confluent/public/site/ssh/{0}.{1}'.format( myname, suffix) + if authorized: + with open(keyname, 'w'): + pass for auth in authorized: - local_key = open(auth, 'r') - dest = open(keyname, 'a') - dest.write(local_key.read()) - local_key.close() - dest.close() + with open(auth, 'r') as local_key: + with open(keyname, 'a') as dest: + dest.write(local_key.read()) if os.path.exists(keyname): os.chmod(keyname, 0o644) os.chown(keyname, neededuid, -1) From 1a40842f0611a5890893c0278836295be3043790 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 15 Aug 2024 10:54:52 -0400 Subject: [PATCH 106/146] Fix osdeploy updateboot to find multiple grub.cfg --- confluent_server/confluent/osimage.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/confluent_server/confluent/osimage.py b/confluent_server/confluent/osimage.py index 5ff28d16..387922fa 100644 --- a/confluent_server/confluent/osimage.py +++ b/confluent_server/confluent/osimage.py @@ -155,11 +155,12 @@ def update_boot_esxi(profiledir, profile, label): def find_glob(loc, fileglob): + grubcfgs = [] for cdir, _, fs in os.walk(loc): for f in fs: if fnmatch(f, fileglob): - return [os.path.join(cdir, f)] - return None + grubcfgs.append(os.path.join(cdir, f)) + return grubcfgs def update_boot_linux(profiledir, profile, label): From d82a98285751ddb0550e782d41f9ef944ce58239 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 16 Aug 2024 10:52:00 -0400 Subject: [PATCH 107/146] Have affluent 'power' status actually at least reaching the service --- .../plugins/hardwaremanagement/affluent.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/confluent_server/confluent/plugins/hardwaremanagement/affluent.py b/confluent_server/confluent/plugins/hardwaremanagement/affluent.py index ea169b3b..8946ae0e 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/affluent.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/affluent.py @@ -45,6 +45,13 @@ class WebClient(object): 'target certificate fingerprint and ' 'pubkeys.tls_hardwaremanager attribute')) return {} + except (socket.gaierror, socket.herror, TimeoutError) as e: + results.put(msg.ConfluentTargetTimeout(e.strerror)) + return {} + except Exception as e: + results.put(msg.ConfluentNodeError(self.node, + repr(e))) + return {} if status == 401: results.put(msg.ConfluentTargetInvalidCredentials(self.node, 'Unable to authenticate')) return {} @@ -115,9 +122,7 @@ def retrieve(nodes, element, configmanager, inputdata): results = queue.LightQueue() workers = set([]) if element == ['power', 'state']: - for node in nodes: - yield msg.PowerState(node=node, state='on') - return + _run_method(retrieve_power, workers, results, configmanager, nodes, element) elif element == ['health', 'hardware']: _run_method(retrieve_health, workers, results, configmanager, nodes, element) elif element[:3] == ['inventory', 'hardware', 'all']: @@ -188,9 +193,15 @@ def retrieve_sensors(configmanager, creds, node, results, element): +def retrieve_power(configmanager, creds, node, results, element): + wc = WebClient(node, configmanager, creds) + hinfo = wc.fetch('/affluent/health', results) + if hinfo: + results.put(msg.PowerState(node=node, state='on')) + def retrieve_health(configmanager, creds, node, results, element): wc = WebClient(node, configmanager, creds) - hinfo = wc.fetch('/affluent/health', results) + hinfo = wc.fetch('/affluent/health', results) if hinfo: results.put(msg.HealthSummary(hinfo.get('health', 'unknown'), name=node)) results.put(msg.SensorReadings(hinfo.get('sensors', []), name=node)) From fb10221e1bc51ffad695598b28bf558145d0c199 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 16 Aug 2024 11:26:52 -0400 Subject: [PATCH 108/146] Amend affluent error handling Be more consistent and informative --- .../confluent/plugins/hardwaremanagement/affluent.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/confluent_server/confluent/plugins/hardwaremanagement/affluent.py b/confluent_server/confluent/plugins/hardwaremanagement/affluent.py index 8946ae0e..ee10bd7d 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/affluent.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/affluent.py @@ -46,7 +46,13 @@ class WebClient(object): 'pubkeys.tls_hardwaremanager attribute')) return {} except (socket.gaierror, socket.herror, TimeoutError) as e: - results.put(msg.ConfluentTargetTimeout(e.strerror)) + results.put(msg.ConfluentTargetTimeout(self.node, str(e))) + return {} + except OSError as e: + if e.errno == 113: + results.put(msg.ConfluentTargetTimeout(self.node)) + else: + results.put(msg.ConfluentTargetTimeout(self.node), str(e)) return {} except Exception as e: results.put(msg.ConfluentNodeError(self.node, From 4640cb194fa5ea43dd037edd54988f2de4c45082 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 19 Aug 2024 13:28:49 -0400 Subject: [PATCH 109/146] Provide workaround for XCC refusal to rename the initial account --- .../confluent/discovery/handlers/xcc.py | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/confluent_server/confluent/discovery/handlers/xcc.py b/confluent_server/confluent/discovery/handlers/xcc.py index d4d67590..ef009b6d 100644 --- a/confluent_server/confluent/discovery/handlers/xcc.py +++ b/confluent_server/confluent/discovery/handlers/xcc.py @@ -408,6 +408,34 @@ class NodeHandler(immhandler.NodeHandler): if user['users_user_name'] == '': return user['users_user_id'] + def create_tmp_account(self, wc): + rsp, status = wc.grab_json_response_with_status('/redfish/v1/AccountService/Accounts') + if status != 200: + raise Exception("Unable to list current accounts") + usednames = set([]) + tmpnam = '6pmu0ezczzcp' + tpass = base64.b64encode(os.urandom(9)).decode() + 'Iw47$' + ntpass = base64.b64encode(os.urandom(9)).decode() + 'Iw47$' + for acct in rsp.get("Members", []): + url = acct.get("@odata.id", None) + if url: + uinfo = wc.grab_json_response(url) + usednames.add(uinfo.get('UserName', None)) + if tmpnam in usednames: + raise Exception("Tmp account already exists") + rsp, status = wc.grab_json_response_with_status( + '/redfish/v1/AccountService/Accounts', + {'UserName': tmpnam, 'Password': tpass, 'RoleId': 'Administrator'}) + if status >= 300: + raise Exception("Failure creating tmp account: " + repr(rsp)) + tmpurl = rsp['@odata.id'] + wc.set_basic_credentials(tmpnam, tpass) + rsp, status = wc.grab_json_response_with_status( + tmpurl, {'Password': ntpass}, method='PATCH') + wc.set_basic_credentials(tmpnam, ntpass) + return tmpurl + + def _setup_xcc_account(self, username, passwd, wc): userinfo = wc.grab_json_response('/api/dataset/imm_users') uid = None @@ -442,16 +470,29 @@ class NodeHandler(immhandler.NodeHandler): wc.grab_json_response('/api/providers/logout') wc.set_basic_credentials(self._currcreds[0], self._currcreds[1]) status = 503 + tries = 2 + tmpaccount = None while status != 200: + tries -= 1 rsp, status = wc.grab_json_response_with_status( '/redfish/v1/AccountService/Accounts/{0}'.format(uid), {'UserName': username}, method='PATCH') if status != 200: rsp = json.loads(rsp) if rsp.get('error', {}).get('code', 'Unknown') in ('Base.1.8.GeneralError', 'Base.1.12.GeneralError', 'Base.1.14.GeneralError'): - eventlet.sleep(4) + if tries: + eventlet.sleep(4) + elif tmpaccount: + wc.grab_json_response_with_status(tmpaccount, method='DELETE') + raise Exception('Failed renaming main account') + else: + tmpaccount = self.create_tmp_account(wc) + tries = 8 else: break + if tmpaccount: + wc.set_basic_credentials(username, passwd) + wc.grab_json_response_with_status(tmpaccount, method='DELETE') self.tmppasswd = None self._currcreds = (username, passwd) return From dd2119c6d950e2d2528c3e2521bfb397abe165f9 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 19 Aug 2024 16:26:48 -0400 Subject: [PATCH 110/146] Ignore very old ssh key file --- confluent_osdeploy/common/profile/scripts/setupssh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/confluent_osdeploy/common/profile/scripts/setupssh b/confluent_osdeploy/common/profile/scripts/setupssh index 3fdf0ef5..eb989bb7 100644 --- a/confluent_osdeploy/common/profile/scripts/setupssh +++ b/confluent_osdeploy/common/profile/scripts/setupssh @@ -3,6 +3,9 @@ [ -f /opt/confluent/bin/apiclient ] && confapiclient=/opt/confluent/bin/apiclient [ -f /etc/confluent/apiclient ] && confapiclient=/etc/confluent/apiclient for pubkey in /etc/ssh/ssh_host*key.pub; do + if [ "$pubkey" = /etc/ssh/ssh_host_key.pub ]; then + continue + fi certfile=${pubkey/.pub/-cert.pub} rm $certfile confluentpython $confapiclient /confluent-api/self/sshcert $pubkey -o $certfile From cbd457b464538b3b8015d26ccf94a10188494c0f Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 21 Aug 2024 09:12:18 -0400 Subject: [PATCH 111/146] Cancel the recvr task on close This avoids stail recvr from sending duplicate data. --- confluent_server/confluent/plugins/console/openbmc.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/confluent_server/confluent/plugins/console/openbmc.py b/confluent_server/confluent/plugins/console/openbmc.py index 519ca2d4..3ff08e5a 100644 --- a/confluent_server/confluent/plugins/console/openbmc.py +++ b/confluent_server/confluent/plugins/console/openbmc.py @@ -119,6 +119,7 @@ class TsmConsole(conapi.Console): self.datacallback = None self.nodeconfig = config self.connected = False + self.recvr = None def recvdata(self): @@ -148,13 +149,16 @@ class TsmConsole(conapi.Console): self.ws.set_verify_callback(kv) self.ws.connect('wss://{0}/console0'.format(self.bmc), host=bmc, cookie='XSRF-TOKEN={0}; SESSION={1}'.format(wc.cookies['XSRF-TOKEN'], wc.cookies['SESSION']), subprotocols=[wc.cookies['XSRF-TOKEN']]) self.connected = True - eventlet.spawn_n(self.recvdata) + self.recvr = eventlet.spawn(self.recvdata) return def write(self, data): self.ws.send(data) def close(self): + if self.recvr: + self.recvr.kill() + self.recvr = None if self.ws: self.ws.close() self.connected = False From e735a12b3ae73dd267291aca6cb7fd5474392af8 Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Thu, 22 Aug 2024 12:38:52 +0200 Subject: [PATCH 112/146] Fix small typo --- confluent_server/bin/confluent_selfcheck | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_server/bin/confluent_selfcheck b/confluent_server/bin/confluent_selfcheck index 64794ae4..f1de6c71 100755 --- a/confluent_server/bin/confluent_selfcheck +++ b/confluent_server/bin/confluent_selfcheck @@ -323,7 +323,7 @@ if __name__ == '__main__': print('{} appears to have networking configuration suitable for IPv6 deployment via: {}'.format(args.node, ",".join(bootablev6nics))) else: emprint(f"{args.node} may not have any viable IP network configuration (check name resolution (DNS or hosts file) " - "and/or net.*ipv4_address, and verify that the deployment serer addresses and subnet mask/prefix length are accurate)") + "and/or net.*ipv4_address, and verify that the deployment server addresses and subnet mask/prefix length are accurate)") if not uuidok and not macok: allok = False emprint(f'{args.node} does not have a uuid or mac address defined in id.uuid or net.*hwaddr, deployment will not work (Example resolution: nodeinventory {args.node} -s)') From 82be3c91d5ed996cbf22a4e1c0705085dfa4964a Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 22 Aug 2024 10:20:04 -0400 Subject: [PATCH 113/146] Add support for relay DHCP Use the relay DHCP agent information as basis for subnet comparison instead of self, when present. Use the candidate prefix length verbatim since the relay will offer no hint as to the 'proper' prefix length for the target segment. --- .../confluent/discovery/protocols/pxe.py | 3 ++- confluent_server/confluent/netutil.py | 12 +++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/confluent_server/confluent/discovery/protocols/pxe.py b/confluent_server/confluent/discovery/protocols/pxe.py index 133d8abd..9e88318b 100644 --- a/confluent_server/confluent/discovery/protocols/pxe.py +++ b/confluent_server/confluent/discovery/protocols/pxe.py @@ -728,9 +728,10 @@ def reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile): repview[1:10] = reqview[1:10] # duplicate txid, hwlen, and others repview[10:11] = b'\x80' # always set broadcast repview[28:44] = reqview[28:44] # copy chaddr field + relayip = reqview[24:28].tobytes() gateway = None netmask = None - niccfg = netutil.get_nic_config(cfg, node, ifidx=info['netinfo']['ifidx']) + niccfg = netutil.get_nic_config(cfg, node, ifidx=info['netinfo']['ifidx'], relayip=relayip) nicerr = niccfg.get('error_msg', False) if nicerr: log.log({'error': nicerr}) diff --git a/confluent_server/confluent/netutil.py b/confluent_server/confluent/netutil.py index c1a9210a..a852d96c 100644 --- a/confluent_server/confluent/netutil.py +++ b/confluent_server/confluent/netutil.py @@ -408,7 +408,7 @@ def noneify(cfgdata): # the ip as reported by recvmsg to match the subnet of that net.* interface # if switch and port available, that should match. def get_nic_config(configmanager, node, ip=None, mac=None, ifidx=None, - serverip=None): + serverip=None, relayipn=b'\x00\x00\x00\x00'): """Fetch network configuration parameters for a nic For a given node and interface, find and retrieve the pertinent network @@ -489,6 +489,10 @@ def get_nic_config(configmanager, node, ip=None, mac=None, ifidx=None, bestsrvbyfam = {} for myaddr in myaddrs: fam, svrip, prefix = myaddr[:3] + if fam == socket.AF_INET and relayipn != b'\x00\x00\x00\x00': + bootsvrip = relayipn + else: + bootsvrip = svrip candsrvs.append((fam, svrip, prefix)) if fam == socket.AF_INET: nver = '4' @@ -508,6 +512,8 @@ def get_nic_config(configmanager, node, ip=None, mac=None, ifidx=None, candip = cfgbyname[candidate].get('ipv{}_address'.format(nver), None) if candip and '/' in candip: candip, candprefix = candip.split('/') + if fam == socket.AF_INET and relayipn != b'\x00\x00\x00\x00': + prefix = int(candprefix) if int(candprefix) != prefix: continue candgw = cfgbyname[candidate].get('ipv{}_gateway'.format(nver), None) @@ -515,7 +521,7 @@ def get_nic_config(configmanager, node, ip=None, mac=None, ifidx=None, try: for inf in socket.getaddrinfo(candip, 0, fam, socket.SOCK_STREAM): candipn = socket.inet_pton(fam, inf[-1][0]) - if ipn_on_same_subnet(fam, svrip, candipn, prefix): + if ipn_on_same_subnet(fam, bootsvrip, candipn, prefix): bestsrvbyfam[fam] = svrip cfgdata['ipv{}_address'.format(nver)] = candip cfgdata['ipv{}_method'.format(nver)] = ipmethod @@ -533,7 +539,7 @@ def get_nic_config(configmanager, node, ip=None, mac=None, ifidx=None, elif candgw: for inf in socket.getaddrinfo(candgw, 0, fam, socket.SOCK_STREAM): candgwn = socket.inet_pton(fam, inf[-1][0]) - if ipn_on_same_subnet(fam, svrip, candgwn, prefix): + if ipn_on_same_subnet(fam, bootsvrip, candgwn, prefix): candgws.append((fam, candgwn, prefix)) if foundaddr: return noneify(cfgdata) From 683a160c20577fcb4d839283391a8f188263e373 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 22 Aug 2024 10:25:15 -0400 Subject: [PATCH 114/146] Amend mistake in parameter name --- confluent_server/confluent/discovery/protocols/pxe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_server/confluent/discovery/protocols/pxe.py b/confluent_server/confluent/discovery/protocols/pxe.py index 9e88318b..dac2d30a 100644 --- a/confluent_server/confluent/discovery/protocols/pxe.py +++ b/confluent_server/confluent/discovery/protocols/pxe.py @@ -731,7 +731,7 @@ def reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile): relayip = reqview[24:28].tobytes() gateway = None netmask = None - niccfg = netutil.get_nic_config(cfg, node, ifidx=info['netinfo']['ifidx'], relayip=relayip) + niccfg = netutil.get_nic_config(cfg, node, ifidx=info['netinfo']['ifidx'], relayipn=relayip) nicerr = niccfg.get('error_msg', False) if nicerr: log.log({'error': nicerr}) From 3d53b7631752b19aa6d40aa2af643b3f549df425 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 22 Aug 2024 14:54:48 -0400 Subject: [PATCH 115/146] Fix relay replies being broadcast instead of unicast --- .../confluent/discovery/protocols/pxe.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/confluent_server/confluent/discovery/protocols/pxe.py b/confluent_server/confluent/discovery/protocols/pxe.py index dac2d30a..ccddbe9c 100644 --- a/confluent_server/confluent/discovery/protocols/pxe.py +++ b/confluent_server/confluent/discovery/protocols/pxe.py @@ -614,7 +614,7 @@ def check_reply(node, info, packet, sock, cfg, reqview, addr): return return reply_dhcp6(node, addr, cfg, packet, cfd, profile, sock) else: - return reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile) + return reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile, sock) def reply_dhcp6(node, addr, cfg, packet, cfd, profile, sock): myaddrs = netutil.get_my_addresses(addr[-1], socket.AF_INET6) @@ -698,7 +698,7 @@ def get_my_duid(): return _myuuid -def reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile): +def reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile, sock=None): replen = 275 # default is going to be 286 # while myipn is describing presumed destination, it's really # vague in the face of aliases, need to convert to ifidx and evaluate @@ -787,6 +787,7 @@ def reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile): myipn = socket.inet_aton(myipn) orepview[12:16] = myipn repview[20:24] = myipn + repview[24:28] = relayip repview[236:240] = b'\x63\x82\x53\x63' repview[240:242] = b'\x35\x01' if rqtype == 1: # if discover, then offer @@ -856,7 +857,10 @@ def reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile): ipinfo = 'without address, served from {0}'.format(myip) log.log({ 'info': 'Offering {0} boot {1} to {2}'.format(boottype, ipinfo, node)}) - send_raw_packet(repview, replen + 28, reqview, info) + if relayip != b'\x00\x00\x00\x00': + sock.sendto(repview[28:28 + replen], (socket.inet_ntoa(relayip), 67)) + else: + send_raw_packet(repview, replen + 28, reqview, info) def send_raw_packet(repview, replen, reqview, info): ifidx = info['netinfo']['ifidx'] @@ -881,9 +885,10 @@ def send_raw_packet(repview, replen, reqview, info): sendto(tsock.fileno(), pkt, replen, 0, ctypes.byref(targ), ctypes.sizeof(targ)) -def ack_request(pkt, rq, info): +def ack_request(pkt, rq, info, sock=None): hwlen = bytearray(rq[2:3].tobytes())[0] hwaddr = rq[28:28+hwlen].tobytes() + relayip = rq[24:28].tobytes() myipn = myipbypeer.get(hwaddr, None) if not myipn or pkt.get(54, None) != myipn: return @@ -902,7 +907,10 @@ def ack_request(pkt, rq, info): repview[12:len(rply)].tobytes()) datasum = ~datasum & 0xffff repview[26:28] = struct.pack('!H', datasum) - send_raw_packet(repview, len(rply), rq, info) + if relayip != b'\x00\x00\x00\x00': + sock.sendto(repview[28:], (socket.inet_ntoa(relayip), 67)) + else: + send_raw_packet(repview, len(rply), rq, info) def consider_discover(info, packet, sock, cfg, reqview, nodeguess, addr=None): if info.get('hwaddr', None) in macmap and info.get('uuid', None): @@ -910,7 +918,7 @@ def consider_discover(info, packet, sock, cfg, reqview, nodeguess, addr=None): elif info.get('uuid', None) in uuidmap: check_reply(uuidmap[info['uuid']], info, packet, sock, cfg, reqview, addr) elif packet.get(53, None) == b'\x03': - ack_request(packet, reqview, info) + ack_request(packet, reqview, info, sock) elif info.get('uuid', None) and info.get('hwaddr', None): if time.time() > ignoremacs.get(info['hwaddr'], 0) + 90: ignoremacs[info['hwaddr']] = time.time() From edc3a3e9f337491849ff73a7cf20ada0b334c826 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 22 Aug 2024 16:39:52 -0400 Subject: [PATCH 116/146] Have confignet fallback to unicast per deploycfg In routed deployments, the scan mechanism will not be available. Fall back to routed access to the deploy server as indicated by deploycfg from install time. --- confluent_osdeploy/common/profile/scripts/confignet | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/confluent_osdeploy/common/profile/scripts/confignet b/confluent_osdeploy/common/profile/scripts/confignet index 650f4eb6..9092f631 100644 --- a/confluent_osdeploy/common/profile/scripts/confignet +++ b/confluent_osdeploy/common/profile/scripts/confignet @@ -455,6 +455,12 @@ if __name__ == '__main__': srvs, _ = apiclient.scan_confluents() doneidxs = set([]) dc = None + if not srvs: # the multicast scan failed, fallback to deploycfg cfg file + with open('/etc/confluent/confluent.deploycfg', 'r') as dci: + for cfgline in dci.read().split('\n'): + if cfgline.startswith('deploy_server:'): + srvs = [cfgline.split()[1]] + break for srv in srvs: try: s = socket.create_connection((srv, 443)) From 5d4f0662d1abd30b6faa3725ca6ec0c0c9323571 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 23 Aug 2024 07:06:37 -0400 Subject: [PATCH 117/146] Fix add_local_repositories for routed IPv4 Routed IPv4 deployment is not guaranteed to have an IPv6 server. In this case the safer bet is to try to just accept the IPv4 anyway. --- .../el8/profiles/default/scripts/add_local_repositories | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_osdeploy/el8/profiles/default/scripts/add_local_repositories b/confluent_osdeploy/el8/profiles/default/scripts/add_local_repositories index fb26d5ef..79b0b6c5 100644 --- a/confluent_osdeploy/el8/profiles/default/scripts/add_local_repositories +++ b/confluent_osdeploy/el8/profiles/default/scripts/add_local_repositories @@ -27,7 +27,7 @@ with open('/etc/confluent/confluent.deploycfg') as dplcfgfile: _, profile = line.split(' ', 1) if line.startswith('ipv4_method: '): _, v4cfg = line.split(' ', 1) -if v4cfg == 'static' or v4cfg =='dhcp': +if v4cfg == 'static' or v4cfg =='dhcp' or not server6: server = server4 if not server: server = '[{}]'.format(server6) From f0c5ac557f9785fea93176997612149b52558ce5 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 23 Aug 2024 13:54:27 -0400 Subject: [PATCH 118/146] Fix and extend Relay DHCP Support Relay DHCP support needed better logging and behavior. It had also broken non-relay clients. --- .../confluent/discovery/protocols/pxe.py | 184 +++++++++++------- 1 file changed, 110 insertions(+), 74 deletions(-) diff --git a/confluent_server/confluent/discovery/protocols/pxe.py b/confluent_server/confluent/discovery/protocols/pxe.py index ccddbe9c..f8f1254f 100644 --- a/confluent_server/confluent/discovery/protocols/pxe.py +++ b/confluent_server/confluent/discovery/protocols/pxe.py @@ -346,7 +346,7 @@ def proxydhcp(handler, nodeguess): profile = None if not myipn: myipn = socket.inet_aton(recv) - profile = get_deployment_profile(node, cfg) + profile, stgprofile = get_deployment_profile(node, cfg) if profile: log.log({ 'info': 'Offering proxyDHCP boot from {0} to {1} ({2})'.format(recv, node, client[0])}) @@ -356,7 +356,7 @@ def proxydhcp(handler, nodeguess): continue if opts.get(77, None) == b'iPXE': if not profile: - profile = get_deployment_profile(node, cfg) + profile, stgprofile = get_deployment_profile(node, cfg) if not profile: log.log({'info': 'No pending profile for {0}, skipping proxyDHCP reply'.format(node)}) continue @@ -385,8 +385,9 @@ def proxydhcp(handler, nodeguess): rpv[268:280] = b'\x3c\x09PXEClient\xff' net4011.sendto(rpv[:281], client) except Exception as e: - tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event, - event=log.Events.stacktrace) + log.logtrace() + # tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event, + # event=log.Events.stacktrace) def start_proxydhcp(handler, nodeguess=None): @@ -453,13 +454,14 @@ def snoop(handler, protocol=None, nodeguess=None): # with try/except if i < 64: continue - _, level, typ = struct.unpack('QII', cmsgarr[:16]) - if level == socket.IPPROTO_IP and typ == IP_PKTINFO: - idx, recv = struct.unpack('II', cmsgarr[16:24]) - recv = ipfromint(recv) - rqv = memoryview(rawbuffer)[:i] if rawbuffer[0] == 1: # Boot request - process_dhcp4req(handler, nodeguess, cfg, net4, idx, recv, rqv) + _, level, typ = struct.unpack('QII', cmsgarr[:16]) + if level == socket.IPPROTO_IP and typ == IP_PKTINFO: + idx, recv = struct.unpack('II', cmsgarr[16:24]) + recv = ipfromint(recv) + rqv = memoryview(rawbuffer)[:i] + client = (ipfromint(clientaddr.sin_addr.s_addr), socket.htons(clientaddr.sin_port)) + process_dhcp4req(handler, nodeguess, cfg, net4, idx, recv, rqv, client) elif netc == net6: recv = 'ff02::1:2' pkt, addr = netc.recvfrom(2048) @@ -476,6 +478,10 @@ def snoop(handler, protocol=None, nodeguess=None): tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event, event=log.Events.stacktrace) + +_mac_to_uuidmap = {} + + def process_dhcp6req(handler, rqv, addr, net, cfg, nodeguess): ip = addr[0] req, disco = v6opts_to_dict(bytearray(rqv[4:])) @@ -501,7 +507,7 @@ def process_dhcp6req(handler, rqv, addr, net, cfg, nodeguess): handler(info) consider_discover(info, req, net, cfg, None, nodeguess, addr) -def process_dhcp4req(handler, nodeguess, cfg, net4, idx, recv, rqv): +def process_dhcp4req(handler, nodeguess, cfg, net4, idx, recv, rqv, client): rq = bytearray(rqv) addrlen = rq[2] if addrlen > 16 or addrlen == 0: @@ -531,7 +537,12 @@ def process_dhcp4req(handler, nodeguess, cfg, net4, idx, recv, rqv): # We will fill out service to have something to byte into, # but the nature of the beast is that we do not have peers, # so that will not be present for a pxe snoop - info = {'hwaddr': netaddr, 'uuid': disco['uuid'], + theuuid = disco['uuid'] + if theuuid: + _mac_to_uuidmap[netaddr] = theuuid + elif netaddr in _mac_to_uuidmap: + theuuid = _mac_to_uuidmap[netaddr] + info = {'hwaddr': netaddr, 'uuid': theuuid, 'architecture': disco['arch'], 'netinfo': {'ifidx': idx, 'recvip': recv, 'txid': txid}, 'services': ('pxe-client',)} @@ -539,7 +550,7 @@ def process_dhcp4req(handler, nodeguess, cfg, net4, idx, recv, rqv): and time.time() > ignoredisco.get(netaddr, 0) + 90): ignoredisco[netaddr] = time.time() handler(info) - consider_discover(info, rqinfo, net4, cfg, rqv, nodeguess) + consider_discover(info, rqinfo, net4, cfg, rqv, nodeguess, requestor=client) @@ -583,29 +594,34 @@ def get_deployment_profile(node, cfg, cfd=None): if not cfd: cfd = cfg.get_node_attributes(node, ('deployment.*', 'collective.managercandidates')) profile = cfd.get(node, {}).get('deployment.pendingprofile', {}).get('value', None) - if not profile: - return None - candmgrs = cfd.get(node, {}).get('collective.managercandidates', {}).get('value', None) - if candmgrs: - try: - candmgrs = noderange.NodeRange(candmgrs, cfg).nodes - except Exception: # fallback to unverified noderange - candmgrs = noderange.NodeRange(candmgrs).nodes - if collective.get_myname() not in candmgrs: - return None - return profile + stgprofile = cfd.get(node, {}).get('deployment.stagedprofile', {}).get('value', None) + if profile or stgprofile: + candmgrs = cfd.get(node, {}).get('collective.managercandidates', {}).get('value', None) + if candmgrs: + try: + candmgrs = noderange.NodeRange(candmgrs, cfg).nodes + except Exception: # fallback to unverified noderange + candmgrs = noderange.NodeRange(candmgrs).nodes + if collective.get_myname() not in candmgrs: + return None, None + return profile, stgprofile staticassigns = {} myipbypeer = {} -def check_reply(node, info, packet, sock, cfg, reqview, addr): - httpboot = info['architecture'] == 'uefi-httpboot' +def check_reply(node, info, packet, sock, cfg, reqview, addr, requestor): + if not requestor: + requestor = ('0.0.0.0', None) + if requestor[0] == '0.0.0.0' and not info.get('uuid', None): + return # ignore DHCP from local non-PXE segment + httpboot = info.get('architecture', None) == 'uefi-httpboot' cfd = cfg.get_node_attributes(node, ('deployment.*', 'collective.managercandidates')) - profile = get_deployment_profile(node, cfg, cfd) - if not profile: + profile, stgprofile = get_deployment_profile(node, cfg, cfd) + if ((not profile) + and (requestor[0] == '0.0.0.0' or not stgprofile)): if time.time() > ignoremacs.get(info['hwaddr'], 0) + 90: ignoremacs[info['hwaddr']] = time.time() log.log({'info': 'Ignoring boot attempt by {0} no deployment profile specified (uuid {1}, hwaddr {2})'.format( - node, info['uuid'], info['hwaddr'] + node, info.get('uuid', 'NA'), info['hwaddr'] )}) return if addr: @@ -614,7 +630,7 @@ def check_reply(node, info, packet, sock, cfg, reqview, addr): return return reply_dhcp6(node, addr, cfg, packet, cfd, profile, sock) else: - return reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile, sock) + return reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile, sock, requestor) def reply_dhcp6(node, addr, cfg, packet, cfd, profile, sock): myaddrs = netutil.get_my_addresses(addr[-1], socket.AF_INET6) @@ -651,14 +667,16 @@ def reply_dhcp6(node, addr, cfg, packet, cfd, profile, sock): ipass[4:16] = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x18' ipass[16:32] = socket.inet_pton(socket.AF_INET6, ipv6addr) ipass[32:40] = b'\x00\x00\x00\x78\x00\x00\x01\x2c' - elif (not packet['vci']) or not packet['vci'].startswith('HTTPClient:Arch:'): - return # do not send ip-less replies to anything but HTTPClient specifically - #1 msgtype - #3 txid - #22 - server ident - #len(packet[1]) + 4 - client ident - #len(ipass) + 4 or 0 - #len(url) + 4 + elif (not packet['vci']) or not packet['vci'].startswith( + 'HTTPClient:Arch:'): + # do not send ip-less replies to anything but HTTPClient specifically + return + # 1 msgtype + # 3 txid + # 22 - server ident + # len(packet[1]) + 4 - client ident + # len(ipass) + 4 or 0 + # len(url) + 4 replylen = 50 + len(bootfile) + len(packet[1]) + 4 if len(ipass): replylen += len(ipass) @@ -698,26 +716,31 @@ def get_my_duid(): return _myuuid -def reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile, sock=None): +def reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile, sock=None, requestor=None): replen = 275 # default is going to be 286 # while myipn is describing presumed destination, it's really # vague in the face of aliases, need to convert to ifidx and evaluate # aliases for best match to guess - + isboot = True + if requestor is None: + requestor = ('0.0.0.0', None) + if info.get('architecture', None) is None: + isboot = False rqtype = packet[53][0] - insecuremode = cfd.get(node, {}).get('deployment.useinsecureprotocols', - {}).get('value', 'never') - if not insecuremode: - insecuremode = 'never' - if insecuremode == 'never' and not httpboot: - if rqtype == 1 and info['architecture']: - log.log( - {'info': 'Boot attempt by {0} detected in insecure mode, but ' - 'insecure mode is disabled. Set the attribute ' - '`deployment.useinsecureprotocols` to `firmware` or ' - '`always` to enable support, or use UEFI HTTP boot ' - 'with HTTPS.'.format(node)}) - return + if isboot: + insecuremode = cfd.get(node, {}).get('deployment.useinsecureprotocols', + {}).get('value', 'never') + if not insecuremode: + insecuremode = 'never' + if insecuremode == 'never' and not httpboot: + if rqtype == 1 and info.get('architecture', None): + log.log( + {'info': 'Boot attempt by {0} detected in insecure mode, but ' + 'insecure mode is disabled. Set the attribute ' + '`deployment.useinsecureprotocols` to `firmware` or ' + '`always` to enable support, or use UEFI HTTP boot ' + 'with HTTPS.'.format(node)}) + return reply = bytearray(512) repview = memoryview(reply) repview[:20] = iphdr @@ -729,6 +752,9 @@ def reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile, sock=N repview[10:11] = b'\x80' # always set broadcast repview[28:44] = reqview[28:44] # copy chaddr field relayip = reqview[24:28].tobytes() + if (not isboot) and relayip == b'\x00\x00\x00\x00': + # Ignore local DHCP packets if it isn't a firmware request + return gateway = None netmask = None niccfg = netutil.get_nic_config(cfg, node, ifidx=info['netinfo']['ifidx'], relayipn=relayip) @@ -755,7 +781,7 @@ def reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile, sock=N gateway = None netmask = (2**32 - 1) ^ (2**(32 - netmask) - 1) netmask = struct.pack('!I', netmask) - elif (not packet['vci']) or not (packet['vci'].startswith('HTTPClient:Arch:') or packet['vci'].startswith('PXEClient')): + elif (not packet.get('vci', None)) or not (packet['vci'].startswith('HTTPClient:Arch:') or packet['vci'].startswith('PXEClient')): return # do not send ip-less replies to anything but netboot specifically myipn = niccfg['deploy_server'] if not myipn: @@ -775,9 +801,9 @@ def reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile, sock=N node, profile, len(bootfile) - 127)}) return repview[108:108 + len(bootfile)] = bootfile - elif info['architecture'] == 'uefi-aarch64' and packet.get(77, None) == b'iPXE': + elif info.get('architecture', None) == 'uefi-aarch64' and packet.get(77, None) == b'iPXE': if not profile: - profile = get_deployment_profile(node, cfg) + profile, stgprofile = get_deployment_profile(node, cfg) if not profile: log.log({'info': 'No pending profile for {0}, skipping proxyDHCP eply'.format(node)}) return @@ -798,17 +824,19 @@ def reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile, sock=N repview[245:249] = myipn repview[249:255] = b'\x33\x04\x00\x00\x00\xf0' # fixed short lease time repview[255:257] = b'\x61\x11' - repview[257:274] = packet[97] + if packet.get(97, None) is not None: + repview[257:274] = packet[97] # Note that sending PXEClient kicks off the proxyDHCP procedure, ignoring # boot filename and such in the DHCP packet # we will simply always do it to provide the boot payload in a consistent # matter to both dhcp-elsewhere and fixed ip clients - if info['architecture'] == 'uefi-httpboot': - repview[replen - 1:replen + 11] = b'\x3c\x0aHTTPClient' - replen += 12 - else: - repview[replen - 1:replen + 10] = b'\x3c\x09PXEClient' - replen += 11 + if isboot: + if info.get('architecture', None) == 'uefi-httpboot': + repview[replen - 1:replen + 11] = b'\x3c\x0aHTTPClient' + replen += 12 + else: + repview[replen - 1:replen + 10] = b'\x3c\x09PXEClient' + replen += 11 hwlen = bytearray(reqview[2:3].tobytes())[0] fulladdr = repview[28:28+hwlen].tobytes() myipbypeer[fulladdr] = myipn @@ -825,13 +853,14 @@ def reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile, sock=N repview[replen - 1:replen + 1] = b'\x03\x04' repview[replen + 1:replen + 5] = gateway replen += 6 + elif relayip != b'\x00\x00\x00\x00': + log.log({'error': 'Relay DHCP offer to {} will fail due to missing gateway information'.format(node)}) if 82 in packet: reloptionslen = len(packet[82]) reloptionshdr = struct.pack('BB', 82, reloptionslen) repview[replen - 1:replen + 1] = reloptionshdr repview[replen + 1:replen + reloptionslen + 1] = packet[82] replen += 2 + reloptionslen - repview[replen - 1:replen] = b'\xff' # end of options, should always be last byte repview = memoryview(reply) pktlen = struct.pack('!H', replen + 28) # ip+udp = 28 @@ -855,13 +884,18 @@ def reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile, sock=N ipinfo = 'with static address {0}'.format(niccfg['ipv4_address']) else: ipinfo = 'without address, served from {0}'.format(myip) - log.log({ - 'info': 'Offering {0} boot {1} to {2}'.format(boottype, ipinfo, node)}) + if isboot: + log.log({ + 'info': 'Offering {0} boot {1} to {2}'.format(boottype, ipinfo, node)}) + else: + log.log({ + 'info': 'Offering DHCP {} to {}'.format(ipinfo, node)}) if relayip != b'\x00\x00\x00\x00': - sock.sendto(repview[28:28 + replen], (socket.inet_ntoa(relayip), 67)) + sock.sendto(repview[28:28 + replen], requestor) else: send_raw_packet(repview, replen + 28, reqview, info) + def send_raw_packet(repview, replen, reqview, info): ifidx = info['netinfo']['ifidx'] tsock = socket.socket(socket.AF_PACKET, socket.SOCK_DGRAM, @@ -885,7 +919,7 @@ def send_raw_packet(repview, replen, reqview, info): sendto(tsock.fileno(), pkt, replen, 0, ctypes.byref(targ), ctypes.sizeof(targ)) -def ack_request(pkt, rq, info, sock=None): +def ack_request(pkt, rq, info, sock=None, requestor=None): hwlen = bytearray(rq[2:3].tobytes())[0] hwaddr = rq[28:28+hwlen].tobytes() relayip = rq[24:28].tobytes() @@ -908,17 +942,19 @@ def ack_request(pkt, rq, info, sock=None): datasum = ~datasum & 0xffff repview[26:28] = struct.pack('!H', datasum) if relayip != b'\x00\x00\x00\x00': - sock.sendto(repview[28:], (socket.inet_ntoa(relayip), 67)) + sock.sendto(repview[28:], requestor) else: send_raw_packet(repview, len(rply), rq, info) -def consider_discover(info, packet, sock, cfg, reqview, nodeguess, addr=None): - if info.get('hwaddr', None) in macmap and info.get('uuid', None): - check_reply(macmap[info['hwaddr']], info, packet, sock, cfg, reqview, addr) +def consider_discover(info, packet, sock, cfg, reqview, nodeguess, addr=None, requestor=None): + if packet.get(53, None) == b'\x03': + ack_request(packet, reqview, info, sock, requestor) + elif info.get('hwaddr', None) in macmap: # and info.get('uuid', None): + check_reply(macmap[info['hwaddr']], info, packet, sock, cfg, reqview, addr, requestor) elif info.get('uuid', None) in uuidmap: - check_reply(uuidmap[info['uuid']], info, packet, sock, cfg, reqview, addr) + check_reply(uuidmap[info['uuid']], info, packet, sock, cfg, reqview, addr, requestor) elif packet.get(53, None) == b'\x03': - ack_request(packet, reqview, info, sock) + ack_request(packet, reqview, info, sock, requestor) elif info.get('uuid', None) and info.get('hwaddr', None): if time.time() > ignoremacs.get(info['hwaddr'], 0) + 90: ignoremacs[info['hwaddr']] = time.time() From c7d87b755ab0183343c04d496b87dd9f6e9c0061 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 23 Aug 2024 18:02:15 -0400 Subject: [PATCH 119/146] Add logic to get remote deployers locked into current nic --- confluent_server/confluent/netutil.py | 40 +++++++++++++++++++++-- confluent_server/confluent/selfservice.py | 2 +- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/confluent_server/confluent/netutil.py b/confluent_server/confluent/netutil.py index a852d96c..b4b1fbb5 100644 --- a/confluent_server/confluent/netutil.py +++ b/confluent_server/confluent/netutil.py @@ -408,7 +408,8 @@ def noneify(cfgdata): # the ip as reported by recvmsg to match the subnet of that net.* interface # if switch and port available, that should match. def get_nic_config(configmanager, node, ip=None, mac=None, ifidx=None, - serverip=None, relayipn=b'\x00\x00\x00\x00'): + serverip=None, relayipn=b'\x00\x00\x00\x00', + clientip=None): """Fetch network configuration parameters for a nic For a given node and interface, find and retrieve the pertinent network @@ -429,6 +430,25 @@ def get_nic_config(configmanager, node, ip=None, mac=None, ifidx=None, #TODO(jjohnson2): ip address, prefix length, mac address, # join a bond/bridge, vlan configs, etc. # also other nic criteria, physical location, driver and index... + clientfam = None + clientipn = None + serverfam = None + serveripn = None + llaipn = socket.inet_pton(socket.AF_INET6, 'fe80::') + if serverip is not None: + if '.' in serverip: + serverfam = socket.AF_INET + elif ':' in serverip: + serverfam = socket.AF_INET6 + if serverfam: + serveripn = socket.inet_pton(serverfam, serverip) + if clientip is not None: + if '.' in clientip: + clientfam = socket.AF_INET + elif ':' in clientip: + clientfam = socket.AF_INET6 + if clientfam: + clientipn = socket.inet_pton(clientfam, clientip) nodenetattribs = configmanager.get_node_attributes( node, 'net*').get(node, {}) cfgbyname = {} @@ -466,9 +486,22 @@ def get_nic_config(configmanager, node, ip=None, mac=None, ifidx=None, cfgdata['ipv4_broken'] = True if v6broken: cfgdata['ipv6_broken'] = True + isremote = False if serverip is not None: dhcprequested = False myaddrs = get_addresses_by_serverip(serverip) + if serverfam == socket.AF_INET6 and ipn_on_same_subnet(serverfam, serveripn, llaipn, 64): + isremote = False + elif clientfam: + for myaddr in myaddrs: + # we may have received over a local vlan, wrong aliased subnet + # so have to check for *any* potential matches + fam, svrip, prefix = myaddr[:3] + if fam == clientfam: + if ipn_on_same_subnet(fam, clientipn, svrip, prefix): + break + else: + isremote = True genericmethod = 'static' ipbynodename = None ip6bynodename = None @@ -514,14 +547,15 @@ def get_nic_config(configmanager, node, ip=None, mac=None, ifidx=None, candip, candprefix = candip.split('/') if fam == socket.AF_INET and relayipn != b'\x00\x00\x00\x00': prefix = int(candprefix) - if int(candprefix) != prefix: + if (not isremote) and int(candprefix) != prefix: continue candgw = cfgbyname[candidate].get('ipv{}_gateway'.format(nver), None) if candip: try: for inf in socket.getaddrinfo(candip, 0, fam, socket.SOCK_STREAM): candipn = socket.inet_pton(fam, inf[-1][0]) - if ipn_on_same_subnet(fam, bootsvrip, candipn, prefix): + if ((isremote and ipn_on_same_subnet(fam, clientipn, candipn, int(candprefix))) + or ipn_on_same_subnet(fam, bootsvrip, candipn, prefix)): bestsrvbyfam[fam] = svrip cfgdata['ipv{}_address'.format(nver)] = candip cfgdata['ipv{}_method'.format(nver)] = ipmethod diff --git a/confluent_server/confluent/selfservice.py b/confluent_server/confluent/selfservice.py index 438d5b3e..b7577b92 100644 --- a/confluent_server/confluent/selfservice.py +++ b/confluent_server/confluent/selfservice.py @@ -282,7 +282,7 @@ def handle_request(env, start_response): ifidx = int(nici.read()) ncfg = netutil.get_nic_config(cfg, nodename, ifidx=ifidx) else: - ncfg = netutil.get_nic_config(cfg, nodename, serverip=myip) + ncfg = netutil.get_nic_config(cfg, nodename, serverip=myip, clientip=clientip) if env['PATH_INFO'] == '/self/deploycfg': for key in list(ncfg): if 'v6' in key: From 77c5b70ad954eebc2916d6924a677f47837cf217 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 26 Aug 2024 09:34:58 -0400 Subject: [PATCH 120/146] Wire up '-a -e' for nodeconfig --- confluent_client/bin/nodeconfig | 11 ++++++++--- .../confluent/plugins/hardwaremanagement/ipmi.py | 5 ++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/confluent_client/bin/nodeconfig b/confluent_client/bin/nodeconfig index 06d512c7..4d3d17f3 100755 --- a/confluent_client/bin/nodeconfig +++ b/confluent_client/bin/nodeconfig @@ -303,9 +303,14 @@ else: '/noderange/{0}/configuration/management_controller/extended/all'.format(noderange), session, printbmc, options, attrprefix='bmc.') if options.extra: - rcode |= client.print_attrib_path( - '/noderange/{0}/configuration/management_controller/extended/extra'.format(noderange), - session, printextbmc, options) + if options.advanced: + rcode |= client.print_attrib_path( + '/noderange/{0}/configuration/management_controller/extended/extra_advanced'.format(noderange), + session, printextbmc, options) + else: + rcode |= client.print_attrib_path( + '/noderange/{0}/configuration/management_controller/extended/extra'.format(noderange), + session, printextbmc, options) if printsys or options.exclude: if printsys == 'all': printsys = [] diff --git a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py index 06a8c444..566cc478 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py @@ -660,6 +660,8 @@ class IpmiHandler(object): return self.handle_bmcconfig(True) elif self.element[1:4] == ['management_controller', 'extended', 'extra']: return self.handle_bmcconfig(True, extended=True) + elif self.element[1:4] == ['management_controller', 'extended', 'extra_advanced']: + return self.handle_bmcconfig(True, advanced=True, extended=True) elif self.element[1:3] == ['system', 'all']: return self.handle_sysconfig() elif self.element[1:3] == ['system', 'advanced']: @@ -1472,7 +1474,8 @@ class IpmiHandler(object): if 'read' == self.op: try: if extended: - bmccfg = self.ipmicmd.get_extended_bmc_configuration() + bmccfg = self.ipmicmd.get_extended_bmc_configuration( + hideadvanced=(not advanced)) else: bmccfg = self.ipmicmd.get_bmc_configuration() self.output.put(msg.ConfigSet(self.node, bmccfg)) From 4f1b6b1facd601f46972d8807fda13ddec4a2cb6 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 26 Aug 2024 09:42:44 -0400 Subject: [PATCH 121/146] Add extra_advanced to core resource tree. --- confluent_server/confluent/core.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/confluent_server/confluent/core.py b/confluent_server/confluent/core.py index ce792fcb..a4e311c4 100644 --- a/confluent_server/confluent/core.py +++ b/confluent_server/confluent/core.py @@ -358,6 +358,10 @@ def _init_core(): 'pluginattrs': ['hardwaremanagement.method'], 'default': 'ipmi', }), + 'extra_advanced': PluginRoute({ + 'pluginattrs': ['hardwaremanagement.method'], + 'default': 'ipmi', + }), }, }, 'storage': { From d84a76dbc6d21d3798abfa9f7d518e59842fa0b0 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 26 Aug 2024 10:01:24 -0400 Subject: [PATCH 122/146] Fix -a and -e nodeconfig --- confluent_server/confluent/plugins/hardwaremanagement/ipmi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py index 566cc478..b53eccb1 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py @@ -659,9 +659,9 @@ class IpmiHandler(object): elif self.element[1:4] == ['management_controller', 'extended', 'advanced']: return self.handle_bmcconfig(True) elif self.element[1:4] == ['management_controller', 'extended', 'extra']: - return self.handle_bmcconfig(True, extended=True) + return self.handle_bmcconfig(advanced=False, extended=True) elif self.element[1:4] == ['management_controller', 'extended', 'extra_advanced']: - return self.handle_bmcconfig(True, advanced=True, extended=True) + return self.handle_bmcconfig(advanced=True, extended=True) elif self.element[1:3] == ['system', 'all']: return self.handle_sysconfig() elif self.element[1:3] == ['system', 'advanced']: From 7304c8e3b7169afdda2b4b606a62c9e974550d8a Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 26 Aug 2024 16:51:35 -0400 Subject: [PATCH 123/146] Fix LLA request for deploycfg If a client uses IPv6 LLA, the '%' location may slip in. In such a case, disable client ip matching, since fe80:: is useless for that anyway. --- confluent_server/confluent/netutil.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/confluent_server/confluent/netutil.py b/confluent_server/confluent/netutil.py index b4b1fbb5..e5384f5d 100644 --- a/confluent_server/confluent/netutil.py +++ b/confluent_server/confluent/netutil.py @@ -443,7 +443,10 @@ def get_nic_config(configmanager, node, ip=None, mac=None, ifidx=None, if serverfam: serveripn = socket.inet_pton(serverfam, serverip) if clientip is not None: - if '.' in clientip: + if '%' in clientip: + # link local, don't even bother' + clientfam = None + elif '.' in clientip: clientfam = socket.AF_INET elif ':' in clientip: clientfam = socket.AF_INET6 From 1d80e2703caab15634aea826ec52a89155e74f0f Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 27 Aug 2024 09:59:16 -0400 Subject: [PATCH 124/146] Add extended nodeconfig to confluent redfish --- .../plugins/hardwaremanagement/redfish.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/confluent_server/confluent/plugins/hardwaremanagement/redfish.py b/confluent_server/confluent/plugins/hardwaremanagement/redfish.py index 2c2857de..f89d9a09 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/redfish.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/redfish.py @@ -516,6 +516,12 @@ class IpmiHandler(object): return self.handle_ntp() elif self.element[1:4] == ['management_controller', 'extended', 'all']: return self.handle_bmcconfig() + elif self.element[1:4] == ['management_controller', 'extended', 'advanced']: + return self.handle_bmcconfig(True) + elif self.element[1:4] == ['management_controller', 'extended', 'extra']: + return self.handle_bmcconfig(advanced=False, extended=True) + elif self.element[1:4] == ['management_controller', 'extended', 'extra_advanced']: + return self.handle_bmcconfig(advanced=True, extended=True) elif self.element[1:3] == ['system', 'all']: return self.handle_sysconfig() elif self.element[1:3] == ['system', 'advanced']: @@ -1312,12 +1318,15 @@ class IpmiHandler(object): if 'read' == self.op: lc = self.ipmicmd.get_location_information() - def handle_bmcconfig(self, advanced=False): + def handle_bmcconfig(self, advanced=False, extended=False): if 'read' == self.op: try: - self.output.put(msg.ConfigSet( - self.node, - self.ipmicmd.get_bmc_configuration())) + if extended: + bmccfg = self.ipmicmd.get_extended_bmc_configuration( + hideadvanced=(not advanced)) + else: + bmccfg = self.ipmicmd.get_bmc_configuration() + self.output.put(msg.ConfigSet(self.node, bmccfg)) except Exception as e: self.output.put( msg.ConfluentNodeError(self.node, str(e))) From b601cd97c7c09b6070b49193c49b8472b832f613 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 27 Aug 2024 10:36:30 -0400 Subject: [PATCH 125/146] Bump pyghmi to a higher explicit version --- confluent_server/builddeb | 4 ++-- confluent_server/confluent_server.spec.tmpl | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/confluent_server/builddeb b/confluent_server/builddeb index a63d9d4f..71080110 100755 --- a/confluent_server/builddeb +++ b/confluent_server/builddeb @@ -36,9 +36,9 @@ if [ "$OPKGNAME" = "confluent-server" ]; then if grep wheezy /etc/os-release; then sed -i 's/^\(Depends:.*\)/\1, python-confluent-client, python-lxml, python-eficompressor, python-pycryptodomex, python-dateutil, python-pyopenssl, python-msgpack/' debian/control elif grep jammy /etc/os-release; then - sed -i 's/^\(Depends:.*\)/\1, confluent-client, python3-lxml, python3-eficompressor, python3-pycryptodome, python3-websocket, python3-msgpack, python3-eventlet, python3-pyparsing, python3-pyghmi, python3-paramiko, python3-pysnmp4, python3-libarchive-c, confluent-vtbufferd, python3-netifaces, python3-yaml, python3-dateutil/' debian/control + sed -i 's/^\(Depends:.*\)/\1, confluent-client, python3-lxml, python3-eficompressor, python3-pycryptodome, python3-websocket, python3-msgpack, python3-eventlet, python3-pyparsing, python3-pyghmi(>=1.5.71), python3-paramiko, python3-pysnmp4, python3-libarchive-c, confluent-vtbufferd, python3-netifaces, python3-yaml, python3-dateutil/' debian/control else - sed -i 's/^\(Depends:.*\)/\1, confluent-client, python3-lxml, python3-eficompressor, python3-pycryptodome, python3-websocket, python3-msgpack, python3-eventlet, python3-pyparsing, python3-pyghmi, python3-paramiko, python3-pysnmp4, python3-libarchive-c, confluent-vtbufferd, python3-netifaces, python3-yaml, python3-dateutil, python3-pyasyncore/' debian/control + sed -i 's/^\(Depends:.*\)/\1, confluent-client, python3-lxml, python3-eficompressor, python3-pycryptodome, python3-websocket, python3-msgpack, python3-eventlet, python3-pyparsing, python3-pyghmi(>=1.5.71), python3-paramiko, python3-pysnmp4, python3-libarchive-c, confluent-vtbufferd, python3-netifaces, python3-yaml, python3-dateutil, python3-pyasyncore/' debian/control fi if grep wheezy /etc/os-release; then echo 'confluent_client python-confluent-client' >> debian/pydist-overrides diff --git a/confluent_server/confluent_server.spec.tmpl b/confluent_server/confluent_server.spec.tmpl index 04e63b21..7f99ee11 100644 --- a/confluent_server/confluent_server.spec.tmpl +++ b/confluent_server/confluent_server.spec.tmpl @@ -14,15 +14,15 @@ Prefix: %{_prefix} BuildArch: noarch Requires: confluent_vtbufferd %if "%{dist}" == ".el7" -Requires: python-pyghmi >= 1.0.34, python-eventlet, python-greenlet, python-pycryptodomex >= 3.4.7, confluent_client == %{version}, python-pyparsing, python-paramiko, python-dnspython, python-netifaces, python2-pyasn1 >= 0.2.3, python-pysnmp >= 4.3.4, python-lxml, python-eficompressor, python-setuptools, python-dateutil, python-websocket-client python2-msgpack python-libarchive-c python-yaml python-monotonic cpio +Requires: python-pyghmi >= 1.5.71, python-eventlet, python-greenlet, python-pycryptodomex >= 3.4.7, confluent_client == %{version}, python-pyparsing, python-paramiko, python-dnspython, python-netifaces, python2-pyasn1 >= 0.2.3, python-pysnmp >= 4.3.4, python-lxml, python-eficompressor, python-setuptools, python-dateutil, python-websocket-client python2-msgpack python-libarchive-c python-yaml python-monotonic cpio %else %if "%{dist}" == ".el8" -Requires: python3-pyghmi >= 1.0.34, python3-eventlet, python3-greenlet, python3-pycryptodomex >= 3.4.7, confluent_client == %{version}, python3-pyparsing, python3-paramiko, python3-dns, python3-netifaces, python3-pyasn1 >= 0.2.3, python3-pysnmp >= 4.3.4, python3-lxml, python3-eficompressor, python3-setuptools, python3-dateutil, python3-enum34, python3-asn1crypto, python3-cffi, python3-pyOpenSSL, python3-websocket-client python3-msgpack python3-libarchive-c python3-yaml openssl iproute cpio +Requires: python3-pyghmi >= 1.5.71, python3-eventlet, python3-greenlet, python3-pycryptodomex >= 3.4.7, confluent_client == %{version}, python3-pyparsing, python3-paramiko, python3-dns, python3-netifaces, python3-pyasn1 >= 0.2.3, python3-pysnmp >= 4.3.4, python3-lxml, python3-eficompressor, python3-setuptools, python3-dateutil, python3-enum34, python3-asn1crypto, python3-cffi, python3-pyOpenSSL, python3-websocket-client python3-msgpack python3-libarchive-c python3-yaml openssl iproute cpio %else %if "%{dist}" == ".el9" -Requires: python3-pyghmi >= 1.0.34, python3-eventlet, python3-greenlet, python3-pycryptodomex >= 3.4.7, confluent_client == %{version}, python3-pyparsing, python3-paramiko, python3-dns, python3-netifaces, python3-pyasn1 >= 0.2.3, python3-pysnmp >= 4.3.4, python3-lxml, python3-eficompressor, python3-setuptools, python3-dateutil, python3-cffi, python3-pyOpenSSL, python3-websocket-client python3-msgpack python3-libarchive-c python3-yaml openssl iproute cpio +Requires: python3-pyghmi >= 1.5.74, python3-eventlet, python3-greenlet, python3-pycryptodomex >= 3.4.7, confluent_client == %{version}, python3-pyparsing, python3-paramiko, python3-dns, python3-netifaces, python3-pyasn1 >= 0.2.3, python3-pysnmp >= 4.3.4, python3-lxml, python3-eficompressor, python3-setuptools, python3-dateutil, python3-cffi, python3-pyOpenSSL, python3-websocket-client python3-msgpack python3-libarchive-c python3-yaml openssl iproute cpio %else -Requires: python3-dbm,python3-pyghmi >= 1.0.34, python3-eventlet, python3-greenlet, python3-pycryptodome >= 3.4.7, confluent_client == %{version}, python3-pyparsing, python3-paramiko, python3-dnspython, python3-netifaces, python3-pyasn1 >= 0.2.3, python3-pysnmp >= 4.3.4, python3-lxml, python3-eficompressor, python3-setuptools, python3-dateutil, python3-cffi, python3-pyOpenSSL, python3-websocket-client python3-msgpack python3-libarchive-c python3-PyYAML openssl iproute +Requires: python3-dbm,python3-pyghmi >= 1.5.71, python3-eventlet, python3-greenlet, python3-pycryptodome >= 3.4.7, confluent_client == %{version}, python3-pyparsing, python3-paramiko, python3-dnspython, python3-netifaces, python3-pyasn1 >= 0.2.3, python3-pysnmp >= 4.3.4, python3-lxml, python3-eficompressor, python3-setuptools, python3-dateutil, python3-cffi, python3-pyOpenSSL, python3-websocket-client python3-msgpack python3-libarchive-c python3-PyYAML openssl iproute %endif %endif %endif From 08a3a0ee766ac5f1a85cf1365ac0ecb76f72c6ad Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 27 Aug 2024 10:43:47 -0400 Subject: [PATCH 126/146] Correct wrong pyghmi version in dependencies --- confluent_server/confluent_server.spec.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_server/confluent_server.spec.tmpl b/confluent_server/confluent_server.spec.tmpl index 7f99ee11..acaf9fc4 100644 --- a/confluent_server/confluent_server.spec.tmpl +++ b/confluent_server/confluent_server.spec.tmpl @@ -20,7 +20,7 @@ Requires: python-pyghmi >= 1.5.71, python-eventlet, python-greenlet, python-pycr Requires: python3-pyghmi >= 1.5.71, python3-eventlet, python3-greenlet, python3-pycryptodomex >= 3.4.7, confluent_client == %{version}, python3-pyparsing, python3-paramiko, python3-dns, python3-netifaces, python3-pyasn1 >= 0.2.3, python3-pysnmp >= 4.3.4, python3-lxml, python3-eficompressor, python3-setuptools, python3-dateutil, python3-enum34, python3-asn1crypto, python3-cffi, python3-pyOpenSSL, python3-websocket-client python3-msgpack python3-libarchive-c python3-yaml openssl iproute cpio %else %if "%{dist}" == ".el9" -Requires: python3-pyghmi >= 1.5.74, python3-eventlet, python3-greenlet, python3-pycryptodomex >= 3.4.7, confluent_client == %{version}, python3-pyparsing, python3-paramiko, python3-dns, python3-netifaces, python3-pyasn1 >= 0.2.3, python3-pysnmp >= 4.3.4, python3-lxml, python3-eficompressor, python3-setuptools, python3-dateutil, python3-cffi, python3-pyOpenSSL, python3-websocket-client python3-msgpack python3-libarchive-c python3-yaml openssl iproute cpio +Requires: python3-pyghmi >= 1.5.71, python3-eventlet, python3-greenlet, python3-pycryptodomex >= 3.4.7, confluent_client == %{version}, python3-pyparsing, python3-paramiko, python3-dns, python3-netifaces, python3-pyasn1 >= 0.2.3, python3-pysnmp >= 4.3.4, python3-lxml, python3-eficompressor, python3-setuptools, python3-dateutil, python3-cffi, python3-pyOpenSSL, python3-websocket-client python3-msgpack python3-libarchive-c python3-yaml openssl iproute cpio %else Requires: python3-dbm,python3-pyghmi >= 1.5.71, python3-eventlet, python3-greenlet, python3-pycryptodome >= 3.4.7, confluent_client == %{version}, python3-pyparsing, python3-paramiko, python3-dnspython, python3-netifaces, python3-pyasn1 >= 0.2.3, python3-pysnmp >= 4.3.4, python3-lxml, python3-eficompressor, python3-setuptools, python3-dateutil, python3-cffi, python3-pyOpenSSL, python3-websocket-client python3-msgpack python3-libarchive-c python3-PyYAML openssl iproute %endif From 4dc54b92d5a45a9fd8f988b90939854f1751596a Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 27 Aug 2024 11:35:39 -0400 Subject: [PATCH 127/146] Correct nodeconsole syntaxwarning --- confluent_client/bin/nodeconsole | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_client/bin/nodeconsole b/confluent_client/bin/nodeconsole index 076913be..f05d5783 100755 --- a/confluent_client/bin/nodeconsole +++ b/confluent_client/bin/nodeconsole @@ -243,7 +243,7 @@ if options.windowed: elif 'Height' in line: window_height = int(line.split(':')[1]) elif '-geometry' in line: - l = re.split(' |x|-|\+', line) + l = re.split(' |x|-|\\+', line) l_nosp = [ele for ele in l if ele.strip()] wmxo = int(l_nosp[1]) wmyo = int(l_nosp[2]) From cd91ed0b94b59e6b991dd482bc308cd4f4c22394 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 27 Aug 2024 15:55:54 -0400 Subject: [PATCH 128/146] Fix escape warning on newer python --- confluent_client/bin/confluent2hosts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_client/bin/confluent2hosts b/confluent_client/bin/confluent2hosts index bbf989b1..b467e5cc 100644 --- a/confluent_client/bin/confluent2hosts +++ b/confluent_client/bin/confluent2hosts @@ -157,7 +157,7 @@ def main(): elif attrib.endswith('.ipv6_address') and val: ip6bynode[node][currnet] = val.split('/', 1)[0] elif attrib.endswith('.hostname'): - namesbynode[node][currnet] = re.split('\s+|,', val) + namesbynode[node][currnet] = re.split(r'\s+|,', val) for node in ip4bynode: mydomain = domainsbynode.get(node, None) for ipdb in (ip4bynode, ip6bynode): From 53b9a13a515c7e09d2b888ea416f3bd3890d8c16 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 28 Aug 2024 19:18:43 -0400 Subject: [PATCH 129/146] Fix different invocations of check_fish Particularly nodediscover register can fail. Those invocations are XCC specific, so the targtype should not matter in those cases. --- confluent_server/confluent/discovery/protocols/ssdp.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/confluent_server/confluent/discovery/protocols/ssdp.py b/confluent_server/confluent/discovery/protocols/ssdp.py index c7063838..5c27473b 100644 --- a/confluent_server/confluent/discovery/protocols/ssdp.py +++ b/confluent_server/confluent/discovery/protocols/ssdp.py @@ -451,7 +451,11 @@ def _find_service(service, target): def check_fish(urldata, port=443, verifycallback=None): if not verifycallback: verifycallback = lambda x: True - url, data, targtype = urldata + try: + url, data, targtype = urldata + except ValueError: + url, data = urldata + targtype = 'service:redfish-bmc' try: wc = webclient.SecureHTTPConnection(_get_svrip(data), port, verifycallback=verifycallback, timeout=1.5) peerinfo = wc.grab_json_response(url, headers={'Accept': 'application/json'}) From fd301c609f3b10d2e46a174c4b9e8eba80f8b8f6 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 29 Aug 2024 08:51:22 -0400 Subject: [PATCH 130/146] Add additional log data to relaydhcp --- confluent_server/confluent/discovery/protocols/pxe.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/confluent_server/confluent/discovery/protocols/pxe.py b/confluent_server/confluent/discovery/protocols/pxe.py index f8f1254f..5e5c98d4 100644 --- a/confluent_server/confluent/discovery/protocols/pxe.py +++ b/confluent_server/confluent/discovery/protocols/pxe.py @@ -755,6 +755,9 @@ def reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile, sock=N if (not isboot) and relayip == b'\x00\x00\x00\x00': # Ignore local DHCP packets if it isn't a firmware request return + relayipa = None + if relayip != b'\x00\x00\x00\x00': + relayipa = socket.inet_ntoa(relayip) gateway = None netmask = None niccfg = netutil.get_nic_config(cfg, node, ifidx=info['netinfo']['ifidx'], relayipn=relayip) @@ -884,6 +887,8 @@ def reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile, sock=N ipinfo = 'with static address {0}'.format(niccfg['ipv4_address']) else: ipinfo = 'without address, served from {0}'.format(myip) + if relayipa: + ipinfo += ' (relayed to {} via {})'.format(relayipa, requestor[0]) if isboot: log.log({ 'info': 'Offering {0} boot {1} to {2}'.format(boottype, ipinfo, node)}) From f4f5b4d1b6869b783af722d601e7c285d036d7d3 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 29 Aug 2024 08:57:39 -0400 Subject: [PATCH 131/146] Suppress bad gateway warning if the client ip is unknown In a scenario where we are doing the 'next-server' next to a real dhcp server, but through relay, the missing gateway would be expected. Rely on the final message about 'no address' as the clue to users that something went wrong on the node side. --- confluent_server/confluent/discovery/protocols/pxe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_server/confluent/discovery/protocols/pxe.py b/confluent_server/confluent/discovery/protocols/pxe.py index 5e5c98d4..64e64c79 100644 --- a/confluent_server/confluent/discovery/protocols/pxe.py +++ b/confluent_server/confluent/discovery/protocols/pxe.py @@ -856,7 +856,7 @@ def reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile, sock=N repview[replen - 1:replen + 1] = b'\x03\x04' repview[replen + 1:replen + 5] = gateway replen += 6 - elif relayip != b'\x00\x00\x00\x00': + elif relayip != b'\x00\x00\x00\x00' and clipn: log.log({'error': 'Relay DHCP offer to {} will fail due to missing gateway information'.format(node)}) if 82 in packet: reloptionslen = len(packet[82]) From 1f6987bafcafd737aa9f9403bdfe9989cb8c9406 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Sat, 31 Aug 2024 07:28:51 -0400 Subject: [PATCH 132/146] Fix nodedeploy -c with profile Remove vestigial statement near end, and put an up front clarification to a user trying to use both '-c' and a profile on the same command line. --- confluent_client/bin/nodedeploy | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/confluent_client/bin/nodedeploy b/confluent_client/bin/nodedeploy index 52e3a7d9..15e78f37 100755 --- a/confluent_client/bin/nodedeploy +++ b/confluent_client/bin/nodedeploy @@ -81,6 +81,12 @@ def main(args): if not args.profile and args.network: sys.stderr.write('Both noderange and a profile name are required arguments to request a network deployment\n') return 1 + if args.clear and args.profile: + sys.stderr.write( + 'The -c/--clear option should not be used with a profile, ' + 'it is a request to not deploy any profile, and will clear ' + 'whatever the current profile is without being specified\n') + return 1 if extra: sys.stderr.write('Unrecognized arguments: ' + repr(extra) + '\n') c = client.Command() @@ -166,8 +172,6 @@ def main(args): ','.join(errnodes))) return 1 rc |= c.simple_noderange_command(args.noderange, '/power/state', 'boot') - if args.network and not args.prepareonly: - return rc return 0 if __name__ == '__main__': From e11c3516e98c451cbc98dbb240456b687a339676 Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Tue, 3 Sep 2024 18:25:15 +0200 Subject: [PATCH 133/146] Create README.md (#162) --- README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..9be6cc60 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# Confluent + +![Python 3](https://img.shields.io/badge/python-3-blue.svg) [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/xcat2/confluent/blob/master/LICENSE) + +Confluent is a software package to handle essential bootstrap and operation of scale-out server configurations. +It supports stateful and stateless deployments for various operating systems. + +Check [this page](https://hpc.lenovo.com/users/documentation/whatisconfluent.html +) for a more detailed list of features. + +Confluent is the modern successor of [xCAT](https://github.com/xcat2/xcat-core). +If you're coming from xCAT, check out [this comparison](https://hpc.lenovo.com/users/documentation/confluentvxcat.html). + +# Documentation + +Confluent documentation is hosted on hpc.lenovo.com: https://hpc.lenovo.com/users/documentation/ + +# Download + +Get the latest version from: https://hpc.lenovo.com/users/downloads/ + +Check release notes on: https://hpc.lenovo.com/users/news/ + +# Open Source License + +Confluent is made available under the Apache 2.0 license: https://opensource.org/license/apache-2-0 + +# Developers + +Want to help? Submit a [Pull Request](https://github.com/xcat2/confluent/pulls). From cb67c8328751736ff3e234305a64aad537d7ba3e Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 4 Sep 2024 08:11:42 -0400 Subject: [PATCH 134/146] iAttempt to be consistent with ~ prerelease versioning We may not know where we are going, but at least bump the minor number. Ultimately we may not be building toward that number, or that number will be used in a different branch, but it can at least handle more cases --- confluent_server/makesetup | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/confluent_server/makesetup b/confluent_server/makesetup index de39ea18..012fe1d4 100755 --- a/confluent_server/makesetup +++ b/confluent_server/makesetup @@ -2,7 +2,11 @@ cd `dirname $0` VERSION=`git describe|cut -d- -f 1` NUMCOMMITS=`git describe|cut -d- -f 2` if [ "$NUMCOMMITS" != "$VERSION" ]; then - VERSION=$VERSION.dev$NUMCOMMITS+g`git describe|cut -d- -f 3` + LASTNUM=$(echo $VERSION|rev|cut -d . -f 1|rev) + LASTNUM=$((LASTNUM+1)) + FIRSTPART=$(echo $VERSION|rev|cut -d . -f 2- |rev) + VERSION=${FIRSTPART}.${LASTNUM} + VERSION=$VERSION~dev$NUMCOMMITS+g`git describe|cut -d- -f 3` fi echo $VERSION > VERSION sed -e "s/#VERSION#/$VERSION/" setup.py.tmpl > setup.py From 97e29a5655ad876d8360d5f8ec075166fe5629fd Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 4 Sep 2024 10:11:47 -0400 Subject: [PATCH 135/146] Change versioning to consistently produce prerelease consistent with rpm and deb --- confluent_client/confluent_client.spec.tmpl | 2 +- confluent_server/confluent_server.spec.tmpl | 2 +- confluent_server/makesetup | 2 +- confluent_vtbufferd/builddeb | 6 +++++- confluent_vtbufferd/buildrpm | 6 +++++- imgutil/builddeb | 6 +++++- imgutil/buildrpm | 6 +++++- 7 files changed, 23 insertions(+), 7 deletions(-) diff --git a/confluent_client/confluent_client.spec.tmpl b/confluent_client/confluent_client.spec.tmpl index 820b0bbb..b26155ea 100644 --- a/confluent_client/confluent_client.spec.tmpl +++ b/confluent_client/confluent_client.spec.tmpl @@ -21,7 +21,7 @@ This package enables python development and command line access to a confluent server. %prep -%setup -n %{name}-%{version} -n %{name}-%{version} +%setup -n %{name}-%{lua: print(string.gsub("#VERSION#", "[~+]", "-"))} %build %if "%{dist}" == ".el7" diff --git a/confluent_server/confluent_server.spec.tmpl b/confluent_server/confluent_server.spec.tmpl index acaf9fc4..284a487d 100644 --- a/confluent_server/confluent_server.spec.tmpl +++ b/confluent_server/confluent_server.spec.tmpl @@ -33,7 +33,7 @@ Url: https://github.com/lenovo/confluent Server for console management and systems management aggregation %prep -%setup -n %{name}-%{version} -n %{name}-%{version} +%setup -n %{name}-%{lua: print(string.gsub("#VERSION#", "[~+]", "-"))} %build %if "%{dist}" == ".el7" diff --git a/confluent_server/makesetup b/confluent_server/makesetup index 012fe1d4..a34438d3 100755 --- a/confluent_server/makesetup +++ b/confluent_server/makesetup @@ -6,7 +6,7 @@ if [ "$NUMCOMMITS" != "$VERSION" ]; then LASTNUM=$((LASTNUM+1)) FIRSTPART=$(echo $VERSION|rev|cut -d . -f 2- |rev) VERSION=${FIRSTPART}.${LASTNUM} - VERSION=$VERSION~dev$NUMCOMMITS+g`git describe|cut -d- -f 3` + VERSION=$VERSION~dev$NUMCOMMITS+`git describe|cut -d- -f 3` fi echo $VERSION > VERSION sed -e "s/#VERSION#/$VERSION/" setup.py.tmpl > setup.py diff --git a/confluent_vtbufferd/builddeb b/confluent_vtbufferd/builddeb index 36f41218..3a98315a 100755 --- a/confluent_vtbufferd/builddeb +++ b/confluent_vtbufferd/builddeb @@ -8,7 +8,11 @@ DSCARGS="--with-python3=True --with-python2=False" VERSION=`git describe|cut -d- -f 1` NUMCOMMITS=`git describe|cut -d- -f 2` if [ "$NUMCOMMITS" != "$VERSION" ]; then - VERSION=$VERSION.dev$NUMCOMMITS.g`git describe|cut -d- -f 3` + LASTNUM=$(echo $VERSION|rev|cut -d . -f 1|rev) + LASTNUM=$((LASTNUM+1)) + FIRSTPART=$(echo $VERSION|rev|cut -d . -f 2- |rev) + VERSION=${FIRSTPART}.${LASTNUM} + VERSION=$VERSION~dev$NUMCOMMITS+`git describe|cut -d- -f 3` fi cd .. rm -rf /tmp/confluent diff --git a/confluent_vtbufferd/buildrpm b/confluent_vtbufferd/buildrpm index 35feec59..9a20844d 100755 --- a/confluent_vtbufferd/buildrpm +++ b/confluent_vtbufferd/buildrpm @@ -1,7 +1,11 @@ VERSION=`git describe|cut -d- -f 1` NUMCOMMITS=`git describe|cut -d- -f 2` if [ "$NUMCOMMITS" != "$VERSION" ]; then - VERSION=$VERSION.dev$NUMCOMMITS.g`git describe|cut -d- -f 3` + LASTNUM=$(echo $VERSION|rev|cut -d . -f 1|rev) + LASTNUM=$((LASTNUM+1)) + FIRSTPART=$(echo $VERSION|rev|cut -d . -f 2- |rev) + VERSION=${FIRSTPART}.${LASTNUM} + VERSION=$VERSION~dev$NUMCOMMITS+`git describe|cut -d- -f 3` fi mkdir -p dist/confluent_vtbufferd-$VERSION cp ../LICENSE NOTICE *.c *.h Makefile dist/confluent_vtbufferd-$VERSION diff --git a/imgutil/builddeb b/imgutil/builddeb index 7e12a6e6..a7cee375 100755 --- a/imgutil/builddeb +++ b/imgutil/builddeb @@ -2,7 +2,11 @@ VERSION=`git describe|cut -d- -f 1` NUMCOMMITS=`git describe|cut -d- -f 2` if [ "$NUMCOMMITS" != "$VERSION" ]; then - VERSION=$VERSION.dev$NUMCOMMITS.g`git describe|cut -d- -f 3` + LASTNUM=$(echo $VERSION|rev|cut -d . -f 1|rev) + LASTNUM=$((LASTNUM+1)) + FIRSTPART=$(echo $VERSION|rev|cut -d . -f 2- |rev) + VERSION=${FIRSTPART}.${LASTNUM} + VERSION=$VERSION~dev$NUMCOMMITS+`git describe|cut -d- -f 3` fi mkdir -p /tmp/confluent-imgutil cp -a * /tmp/confluent-imgutil diff --git a/imgutil/buildrpm b/imgutil/buildrpm index 87631ac0..4439b7ed 100755 --- a/imgutil/buildrpm +++ b/imgutil/buildrpm @@ -2,7 +2,11 @@ VERSION=`git describe|cut -d- -f 1` NUMCOMMITS=`git describe|cut -d- -f 2` if [ "$NUMCOMMITS" != "$VERSION" ]; then - VERSION=$VERSION.dev$NUMCOMMITS.g`git describe|cut -d- -f 3` + LASTNUM=$(echo $VERSION|rev|cut -d . -f 1|rev) + LASTNUM=$((LASTNUM+1)) + FIRSTPART=$(echo $VERSION|rev|cut -d . -f 2- |rev) + VERSION=${FIRSTPART}.${LASTNUM} + VERSION=$VERSION~dev$NUMCOMMITS+`git describe|cut -d- -f 3` fi sed -e "s/#VERSION#/$VERSION/" confluent_imgutil.spec.tmpl > confluent_imgutil.spec cp ../LICENSE . From 4a2e943f8432a9531253c4a7e3ac60fc7616778e Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 4 Sep 2024 10:19:11 -0400 Subject: [PATCH 136/146] Update osdeploy rpms to new version scheme for snapshots --- confluent_osdeploy/buildrpm | 6 +++++- confluent_osdeploy/buildrpm-aarch64 | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/confluent_osdeploy/buildrpm b/confluent_osdeploy/buildrpm index 6bd1d419..9e9cb582 100755 --- a/confluent_osdeploy/buildrpm +++ b/confluent_osdeploy/buildrpm @@ -1,7 +1,11 @@ VERSION=`git describe|cut -d- -f 1` NUMCOMMITS=`git describe|cut -d- -f 2` if [ "$NUMCOMMITS" != "$VERSION" ]; then - VERSION=$VERSION.dev$NUMCOMMITS.g`git describe|cut -d- -f 3` + LASTNUM=$(echo $VERSION|rev|cut -d . -f 1|rev) + LASTNUM=$((LASTNUM+1)) + FIRSTPART=$(echo $VERSION|rev|cut -d . -f 2- |rev) + VERSION=${FIRSTPART}.${LASTNUM} + VERSION=$VERSION~dev$NUMCOMMITS+`git describe|cut -d- -f 3` fi sed -e "s/#VERSION#/$VERSION/" confluent_osdeploy.spec.tmpl > confluent_osdeploy.spec cd .. diff --git a/confluent_osdeploy/buildrpm-aarch64 b/confluent_osdeploy/buildrpm-aarch64 index 83ffc519..c269284b 100644 --- a/confluent_osdeploy/buildrpm-aarch64 +++ b/confluent_osdeploy/buildrpm-aarch64 @@ -2,7 +2,11 @@ cd $(dirname $0) VERSION=`git describe|cut -d- -f 1` NUMCOMMITS=`git describe|cut -d- -f 2` if [ "$NUMCOMMITS" != "$VERSION" ]; then - VERSION=$VERSION.dev$NUMCOMMITS.g`git describe|cut -d- -f 3` + LASTNUM=$(echo $VERSION|rev|cut -d . -f 1|rev) + LASTNUM=$((LASTNUM+1)) + FIRSTPART=$(echo $VERSION|rev|cut -d . -f 2- |rev) + VERSION=${FIRSTPART}.${LASTNUM} + VERSION=$VERSION~dev$NUMCOMMITS+`git describe|cut -d- -f 3` fi sed -e "s/#VERSION#/$VERSION/" confluent_osdeploy-aarch64.spec.tmpl > confluent_osdeploy-aarch64.spec cd .. From ee067fa3c065a62ac8c2c3d66ed5eeea2de0b787 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 4 Sep 2024 10:27:00 -0400 Subject: [PATCH 137/146] Cleanup stray debian content in apt --- confluent_server/builddeb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_server/builddeb b/confluent_server/builddeb index 71080110..9aca0d0a 100755 --- a/confluent_server/builddeb +++ b/confluent_server/builddeb @@ -74,7 +74,7 @@ else rm -rf $PKGNAME.egg-info dist setup.py rm -rf $(find deb_dist -mindepth 1 -maxdepth 1 -type d) if [ ! -z "$1" ]; then - mv deb_dist/* $1/ + mv deb_dist/*.deb $1/ fi fi exit 0 From 7c8f85eb065db88066a041a20d32d8db8aac8f01 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 4 Sep 2024 10:53:05 -0400 Subject: [PATCH 138/146] Handle python mangling of filename consistently for rpm build --- confluent_client/confluent_client.spec.tmpl | 8 ++++++-- confluent_server/confluent_server.spec.tmpl | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/confluent_client/confluent_client.spec.tmpl b/confluent_client/confluent_client.spec.tmpl index b26155ea..ee786175 100644 --- a/confluent_client/confluent_client.spec.tmpl +++ b/confluent_client/confluent_client.spec.tmpl @@ -1,12 +1,16 @@ %define name confluent_client %define version #VERSION# +%define fversion %{lua: +sv, _ = string.gsub("#VERSION#", "[~+]", "-") +print(sv) +} %define release 1 Summary: Client libraries and utilities for confluent Name: %{name} Version: %{version} Release: %{release} -Source0: %{name}-%{version}.tar.gz +Source0: %{name}-%{fversion}.tar.gz License: Apache2 Group: Development/Libraries BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot @@ -21,7 +25,7 @@ This package enables python development and command line access to a confluent server. %prep -%setup -n %{name}-%{lua: print(string.gsub("#VERSION#", "[~+]", "-"))} +%setup -n %{name}-%{fversion} %build %if "%{dist}" == ".el7" diff --git a/confluent_server/confluent_server.spec.tmpl b/confluent_server/confluent_server.spec.tmpl index 284a487d..0a36390c 100644 --- a/confluent_server/confluent_server.spec.tmpl +++ b/confluent_server/confluent_server.spec.tmpl @@ -1,12 +1,16 @@ %define name confluent_server %define version #VERSION# +%define fversion %{lua: +sv, _ = string.gsub("#VERSION#", "[~+]", "-") +print(sv) +} %define release 1 Summary: confluent systems management server Name: %{name} Version: %{version} Release: %{release} -Source0: %{name}-%{version}.tar.gz +Source0: %{name}-%{fversion}.tar.gz License: Apache2 Group: Development/Libraries BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot @@ -33,7 +37,7 @@ Url: https://github.com/lenovo/confluent Server for console management and systems management aggregation %prep -%setup -n %{name}-%{lua: print(string.gsub("#VERSION#", "[~+]", "-"))} +%setup -n %{name}-%{fversion} %build %if "%{dist}" == ".el7" From 2c76d94a6debf504ff4c78161be2de60f9b4f359 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 4 Sep 2024 12:13:40 -0400 Subject: [PATCH 139/146] Vary version for debian build process --- confluent_server/builddeb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/confluent_server/builddeb b/confluent_server/builddeb index 9aca0d0a..3a983694 100755 --- a/confluent_server/builddeb +++ b/confluent_server/builddeb @@ -17,7 +17,9 @@ cd /tmp/confluent/$PKGNAME if [ -x ./makeman ]; then ./makeman fi -./makesetup +sed -e 's/~/./' ./makesetup > ./makesetup.deb +chmod +x ./makesetup.deb +./makesetup.deb VERSION=`cat VERSION` cat > setup.cfg << EOF [install] From d17d5341f11325141a62a03070d3e12e65859072 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 4 Sep 2024 19:16:41 -0400 Subject: [PATCH 140/146] Imprement basic vinz management --- confluent_server/confluent/core.py | 1 + confluent_server/confluent/messages.py | 13 +- confluent_server/confluent/vinzmanager.py | 166 ++++++++++++++++++++++ 3 files changed, 176 insertions(+), 4 deletions(-) create mode 100644 confluent_server/confluent/vinzmanager.py diff --git a/confluent_server/confluent/core.py b/confluent_server/confluent/core.py index a4e311c4..0ab6b647 100644 --- a/confluent_server/confluent/core.py +++ b/confluent_server/confluent/core.py @@ -430,6 +430,7 @@ def _init_core(): 'pluginattrs': ['hardwaremanagement.method'], 'default': 'ipmi', }), + 'ikvm': PluginRoute({'handler': 'ikvm'}), }, 'description': PluginRoute({ 'pluginattrs': ['hardwaremanagement.method'], diff --git a/confluent_server/confluent/messages.py b/confluent_server/confluent/messages.py index ce36344d..fa8dbbcb 100644 --- a/confluent_server/confluent/messages.py +++ b/confluent_server/confluent/messages.py @@ -262,10 +262,10 @@ class Generic(ConfluentMessage): def json(self): return json.dumps(self.data) - + def raw(self): return self.data - + def html(self): return json.dumps(self.data) @@ -344,10 +344,10 @@ class ConfluentResourceCount(ConfluentMessage): self.myargs = [count] self.desc = 'Resource Count' self.kvpairs = {'count': count} - + def strip_node(self, node): pass - + class CreatedResource(ConfluentMessage): notnode = True readonly = True @@ -569,6 +569,8 @@ def get_input_message(path, operation, inputdata, nodes=None, multinode=False, return InputLicense(path, nodes, inputdata, configmanager) elif path == ['deployment', 'ident_image']: return InputIdentImage(path, nodes, inputdata) + elif path == ['console', 'ikvm']: + return InputIkvmParams(path, nodes, inputdata) elif inputdata: raise exc.InvalidArgumentException( 'No known input handler for request') @@ -936,6 +938,9 @@ class InputIdentImage(ConfluentInputMessage): keyname = 'ident_image' valid_values = ['create'] +class InputIkvmParams(ConfluentInputMessage): + keyname = 'method' + valid_values = ['unix', 'wss'] class InputIdentifyMessage(ConfluentInputMessage): valid_values = set([ diff --git a/confluent_server/confluent/vinzmanager.py b/confluent_server/confluent/vinzmanager.py new file mode 100644 index 00000000..0eb2314a --- /dev/null +++ b/confluent_server/confluent/vinzmanager.py @@ -0,0 +1,166 @@ + +import confluent.auth as auth +import eventlet +import confluent.messages as msg +import confluent.exceptions as exc +import confluent.util as util +import confluent.config.configmanager as configmanager +import struct +import eventlet.green.socket as socket +import eventlet.green.subprocess as subprocess +import base64 +import os +import pwd +mountsbyuser = {} +_vinzfd = None +_vinztoken = None +webclient = eventlet.import_patched('pyghmi.util.webclient') + + +# Handle the vinz VNC session +def assure_vinz(): + global _vinzfd + global _vinztoken + if _vinzfd is None: + _vinztoken = base64.b64encode(os.urandom(33), altchars=b'_-').decode() + os.environ['VINZ_TOKEN'] = _vinztoken + os.makedirs('/var/run/confluent/vinz/sessions', exist_ok=True) + + _vinzfd = subprocess.Popen( + ['/opt/confluent/bin/vinz', + '-c', '/var/run/confluent/vinz/control', + '-w', '127.0.0.1:4007', + '-a', '/var/run/confluent/vinz/approval', + # vinz supports unix domain websocket, however apache reverse proxy is dicey that way in some versions + '-d', '/var/run/confluent/vinz/sessions']) + while not os.path.exists('/var/run/confluent/vinz/control'): + eventlet.sleep(0.5) + eventlet.spawn(monitor_requests) + + +def get_url(nodename, inputdata): + method = inputdata.inputbynode[nodename] + assure_vinz() + if method == 'wss': + return f'/vinz/kvmsession/{nodename}' + elif method == 'unix': + return request_session(nodename) + + +def send_grant(conn, nodename): + cfg = configmanager.ConfigManager(None) + c = cfg.get_node_attributes( + nodename, + ['secret.hardwaremanagementuser', + 'secret.hardwaremanagementpassword', + 'hardwaremanagement.manager'], decrypt=True) + bmcuser = c.get(nodename, {}).get( + 'secret.hardwaremanagementuser', {}).get('value', None) + bmcpass = c.get(nodename, {}).get( + 'secret.hardwaremanagementpassword', {}).get('value', None) + bmc = c.get(nodename, {}).get( + 'hardwaremanagement.manager', {}).get('value', None) + if bmcuser and bmcpass and bmc: + kv = util.TLSCertVerifier(cfg, nodename, + 'pubkeys.tls_hardwaremanager').verify_cert + wc = webclient.SecureHTTPConnection(bmc, 443, verifycallback=kv) + if not isinstance(bmcuser, str): + bmcuser = bmcuser.decode() + if not isinstance(bmcpass, str): + bmcpass = bmcpass.decode() + rsp = wc.grab_json_response_with_status( + '/login', {'data': [bmcuser, bmcpass]}, + headers={'Content-Type': 'application/json', + 'Accept': 'application/json'}) + sessionid = wc.cookies['SESSION'] + sessiontok = wc.cookies['XSRF-TOKEN'] + url = '/kvm/0' + fprintinfo = cfg.get_node_attributes(nodename, 'pubkeys.tls_hardwaremanager') + fprint = fprintinfo.get( + nodename, {}).get('pubkeys.tls_hardwaremanager', {}).get('value', None) + if not fprint: + return + fprint = fprint.split('$', 1)[1] + fprint = bytes.fromhex(fprint) + conn.send(struct.pack('!BI', 1, len(bmc))) + conn.send(bmc.encode()) + conn.send(struct.pack('!I', len(sessionid))) + conn.send(sessionid.encode()) + conn.send(struct.pack('!I', len(sessiontok))) + conn.send(sessiontok.encode()) + conn.send(struct.pack('!I', len(fprint))) + conn.send(fprint) + conn.send(struct.pack('!I', len(url))) + conn.send(url.encode()) + conn.send(b'\xff') + +def evaluate_request(conn): + try: + creds = conn.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, + struct.calcsize('iII')) + pid, uid, gid = struct.unpack('iII', creds) + if uid != os.getuid(): + return + rqcode, fieldlen = struct.unpack('!BI', conn.recv(5)) + if rqcode != 1: + return + authtoken = conn.recv(fieldlen).decode() + if authtoken != _vinztoken: + return + fieldlen = struct.unpack('!I', conn.recv(4))[0] + nodename = conn.recv(fieldlen).decode() + idtype = struct.unpack('!B', conn.recv(1))[0] + if idtype == 1: + usernum = struct.unpack('!I', conn.recv(4))[0] + if usernum == 0: # root is a special guy + send_grant(conn, nodename) + return + try: + authname = pwd.getpwuid(usernum).pw_name + except Exception: + return + allow = auth.authorize(authname, f'/nodes/{nodename}/console/ikvm') + if not allow: + return + send_grant(conn, nodename) + else: + return + if conn.recv(1) != b'\xff': + return + + finally: + conn.close() + +def monitor_requests(): + a = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + try: + os.remove('/var/run/confluent/vinz/approval') + except Exception: + pass + a.bind('/var/run/confluent/vinz/approval') + os.chmod('/var/run/confluent/vinz/approval', 0o600) + a.listen(8) + while True: + conn, addr = a.accept() + eventlet.spawn_n(evaluate_request, conn) + +def request_session(nodename): + assure_vinz() + a = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + a.connect('/var/run/confluent/vinz/control') + nodename = nodename.encode() + a.send(struct.pack('!BI', 1, len(nodename))) + a.send(nodename) + a.send(b'\xff') + rsp = a.recv(1) + retcode = struct.unpack('!B', rsp)[0] + if retcode != 1: + raise Exception("Bad return code") + rsp = a.recv(4) + nlen = struct.unpack('!I', rsp)[0] + sockname = a.recv(nlen).decode('utf8') + retcode = a.recv(1) + if retcode != b'\xff': + raise Exception("Unrecognized response") + return os.path.join('/var/run/confluent/vinz/sessions', sockname) + From f8715f4cb19a67cf520a9fb444ce7114c86496cd Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 5 Sep 2024 11:42:52 -0400 Subject: [PATCH 141/146] Implement logout on disconnect notification for vinz --- confluent_server/confluent/vinzmanager.py | 78 +++++++++++++++++------ 1 file changed, 57 insertions(+), 21 deletions(-) diff --git a/confluent_server/confluent/vinzmanager.py b/confluent_server/confluent/vinzmanager.py index 0eb2314a..991ba0be 100644 --- a/confluent_server/confluent/vinzmanager.py +++ b/confluent_server/confluent/vinzmanager.py @@ -46,6 +46,36 @@ def get_url(nodename, inputdata): elif method == 'unix': return request_session(nodename) +_usersessions = {} +def close_session(sessionid): + sessioninfo = _usersessions.get(sessionid, None) + if not sessioninfo: + return + del _usersessions[sessionid] + nodename = sessioninfo['nodename'] + wc = sessioninfo['webclient'] + cfg = configmanager.ConfigManager(None) + c = cfg.get_node_attributes( + nodename, + ['secret.hardwaremanagementuser', + 'secret.hardwaremanagementpassword', + ], decrypt=True) + bmcuser = c.get(nodename, {}).get( + 'secret.hardwaremanagementuser', {}).get('value', None) + bmcpass = c.get(nodename, {}).get( + 'secret.hardwaremanagementpassword', {}).get('value', None) + if not isinstance(bmcuser, str): + bmcuser = bmcuser.decode() + if not isinstance(bmcpass, str): + bmcpass = bmcpass.decode() + if bmcuser and bmcpass: + wc.grab_json_response_with_status( + '/logout', {'data': [bmcuser, bmcpass]}, + headers={ + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-XSRF-TOKEN': wc.cookies['XSRF-TOKEN']}) + def send_grant(conn, nodename): cfg = configmanager.ConfigManager(None) @@ -74,6 +104,10 @@ def send_grant(conn, nodename): 'Accept': 'application/json'}) sessionid = wc.cookies['SESSION'] sessiontok = wc.cookies['XSRF-TOKEN'] + _usersessions[sessionid] = { + 'webclient': wc, + 'nodename': nodename, + } url = '/kvm/0' fprintinfo = cfg.get_node_attributes(nodename, 'pubkeys.tls_hardwaremanager') fprint = fprintinfo.get( @@ -102,32 +136,34 @@ def evaluate_request(conn): if uid != os.getuid(): return rqcode, fieldlen = struct.unpack('!BI', conn.recv(5)) - if rqcode != 1: - return authtoken = conn.recv(fieldlen).decode() if authtoken != _vinztoken: return - fieldlen = struct.unpack('!I', conn.recv(4))[0] - nodename = conn.recv(fieldlen).decode() - idtype = struct.unpack('!B', conn.recv(1))[0] - if idtype == 1: - usernum = struct.unpack('!I', conn.recv(4))[0] - if usernum == 0: # root is a special guy + if rqcode == 2: # disconnect notification + fieldlen = struct.unpack('!I', conn.recv(4))[0] + sessionid = conn.recv(fieldlen).decode() + close_session(sessionid) + conn.recv(1) # digest 0xff + if rqcode == 1: # request for new connection + fieldlen = struct.unpack('!I', conn.recv(4))[0] + nodename = conn.recv(fieldlen).decode() + idtype = struct.unpack('!B', conn.recv(1))[0] + if idtype == 1: + usernum = struct.unpack('!I', conn.recv(4))[0] + if usernum == 0: # root is a special guy + send_grant(conn, nodename) + return + try: + authname = pwd.getpwuid(usernum).pw_name + except Exception: + return + allow = auth.authorize(authname, f'/nodes/{nodename}/console/ikvm') + if not allow: + return send_grant(conn, nodename) + else: return - try: - authname = pwd.getpwuid(usernum).pw_name - except Exception: - return - allow = auth.authorize(authname, f'/nodes/{nodename}/console/ikvm') - if not allow: - return - send_grant(conn, nodename) - else: - return - if conn.recv(1) != b'\xff': - return - + conn.recv(1) # should be 0xff finally: conn.close() From c048439849198f039b0fb6d607118172697327ff Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 5 Sep 2024 11:44:29 -0400 Subject: [PATCH 142/146] Reuse existing vinz unix session for a node --- confluent_server/confluent/vinzmanager.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/confluent_server/confluent/vinzmanager.py b/confluent_server/confluent/vinzmanager.py index 991ba0be..75b51294 100644 --- a/confluent_server/confluent/vinzmanager.py +++ b/confluent_server/confluent/vinzmanager.py @@ -37,14 +37,17 @@ def assure_vinz(): eventlet.sleep(0.5) eventlet.spawn(monitor_requests) - +_unix_by_nodename = {} def get_url(nodename, inputdata): method = inputdata.inputbynode[nodename] assure_vinz() if method == 'wss': return f'/vinz/kvmsession/{nodename}' elif method == 'unix': - return request_session(nodename) + if nodename not in _unix_by_nodename: + _unix_by_nodename[nodename] = request_session(nodename) + return _unix_by_nodename[nodename] + _usersessions = {} def close_session(sessionid): From 0fb19ce26360dbf22a358212b798647009370ed8 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 6 Sep 2024 13:41:50 -0400 Subject: [PATCH 143/146] Wire up http sessions to vinzmanager --- confluent_server/confluent/httpapi.py | 14 ++++++++++++++ confluent_server/confluent/vinzmanager.py | 20 ++++++++++++++++---- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/confluent_server/confluent/httpapi.py b/confluent_server/confluent/httpapi.py index 10ab8b86..cf6a42ee 100644 --- a/confluent_server/confluent/httpapi.py +++ b/confluent_server/confluent/httpapi.py @@ -72,6 +72,20 @@ opmap = { } +def get_user_for_session(sessionid, sessiontok): + if not isinstance(sessionid, str): + sessionid = sessionid.decode() + if not isinstance(sessiontok, str): + sessiontok = sessiontok.decode() + if not sessiontok or not sessionid: + raise Exception("invalid session id or token") + if sessiontok != httpsessions.get(sessionid, {}).get('csrftoken', None): + raise Exception("Invalid csrf token for session") + user = httpsessions[sessionid]['name'] + if not isinstance(user, str): + user = user.decode() + return user + def group_creation_resources(): yield confluent.messages.Attributes( kv={'name': None}, desc="Name of the group").html() + '
' diff --git a/confluent_server/confluent/vinzmanager.py b/confluent_server/confluent/vinzmanager.py index 75b51294..308acb59 100644 --- a/confluent_server/confluent/vinzmanager.py +++ b/confluent_server/confluent/vinzmanager.py @@ -11,6 +11,7 @@ import eventlet.green.subprocess as subprocess import base64 import os import pwd +import confluent.httpapi as httpapi mountsbyuser = {} _vinzfd = None _vinztoken = None @@ -44,7 +45,7 @@ def get_url(nodename, inputdata): if method == 'wss': return f'/vinz/kvmsession/{nodename}' elif method == 'unix': - if nodename not in _unix_by_nodename: + if nodename not in _unix_by_nodename or not os.path.exists(_unix_by_nodename[nodename]): _unix_by_nodename[nodename] = request_session(nodename) return _unix_by_nodename[nodename] @@ -132,6 +133,8 @@ def send_grant(conn, nodename): conn.send(b'\xff') def evaluate_request(conn): + allow = False + authname = None try: creds = conn.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, struct.calcsize('iII')) @@ -160,13 +163,22 @@ def evaluate_request(conn): authname = pwd.getpwuid(usernum).pw_name except Exception: return - allow = auth.authorize(authname, f'/nodes/{nodename}/console/ikvm') - if not allow: + elif idtype == 2: + fieldlen = struct.unpack('!I', conn.recv(4))[0] + sessionid = conn.recv(fieldlen) + fieldlen = struct.unpack('!I', conn.recv(4))[0] + sessiontok = conn.recv(fieldlen) + try: + authname = httpapi.get_user_for_session(sessionid, sessiontok) + except Exception: return - send_grant(conn, nodename) else: return conn.recv(1) # should be 0xff + if authname: + allow = auth.authorize(authname, f'/nodes/{nodename}/console/ikvm') + if allow: + send_grant(conn, nodename) finally: conn.close() From 8704afcee5f0aa7f38f7923cdfe82120b536c380 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 10 Sep 2024 11:28:00 -0400 Subject: [PATCH 144/146] Add glue to confluent api from vinzmanager --- .../confluent/plugins/console/ikvm.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 confluent_server/confluent/plugins/console/ikvm.py diff --git a/confluent_server/confluent/plugins/console/ikvm.py b/confluent_server/confluent/plugins/console/ikvm.py new file mode 100644 index 00000000..395ead60 --- /dev/null +++ b/confluent_server/confluent/plugins/console/ikvm.py @@ -0,0 +1,34 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2024 Lenovo +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +# This provides linkage between vinz and confluent, with support +# for getting session authorization from the BMC + +import confluent.vinzmanager as vinzmanager +import confluent.messages as msg + + +def create(nodes, element, configmanager, inputdata): + for node in nodes: + url = vinzmanager.get_url(node, inputdata) + yield msg.ChildCollection(url) + + +def update(nodes, element, configmanager, inputdata): + for node in nodes: + url = vinzmanager.get_url(node, inputdata) + yield msg.ChildCollection(url) From 7da3944b2b7b0720f36cd7ac9961ffbb139652d9 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 11 Sep 2024 09:13:30 -0400 Subject: [PATCH 145/146] Allow blink for ipmi OEM IPMI may now do blink --- .../confluent/plugins/hardwaremanagement/ipmi.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py index b53eccb1..32fabefe 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py @@ -1378,10 +1378,8 @@ class IpmiHandler(object): def identify(self): if 'update' == self.op: identifystate = self.inputdata.inputbynode[self.node] == 'on' - if self.inputdata.inputbynode[self.node] == 'blink': - raise exc.InvalidArgumentException( - '"blink" is not supported with ipmi') - self.ipmicmd.set_identify(on=identifystate) + blinkstate = self.inputdata.inputbynode[self.node] == 'blink' + self.ipmicmd.set_identify(on=identifystate, blink=blinkstate) self.output.put(msg.IdentifyState( node=self.node, state=self.inputdata.inputbynode[self.node])) return From d553ab864bbcc3da82db8a316b825aa76b133014 Mon Sep 17 00:00:00 2001 From: tkucherera Date: Thu, 12 Sep 2024 10:27:05 -0400 Subject: [PATCH 146/146] resolve merge conflicts --- confluent_server/confluent/core.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/confluent_server/confluent/core.py b/confluent_server/confluent/core.py index 55456453..ec62c57d 100644 --- a/confluent_server/confluent/core.py +++ b/confluent_server/confluent/core.py @@ -70,6 +70,7 @@ import eventlet.green.socket as socket import struct import sys import uuid +import yaml pluginmap = {} dispatch_plugins = (b'ipmi', u'ipmi', b'redfish', u'redfish', b'tsmsol', u'tsmsol', b'geist', u'geist', b'deltapdu', u'deltapdu', b'eatonpdu', u'eatonpdu', b'affluent', u'affluent', b'cnos', u'cnos') @@ -160,7 +161,7 @@ def _merge_dict(original, custom): rootcollections = ['deployment/', 'discovery/', 'events/', 'networking/', - 'noderange/', 'nodes/', 'nodegroups/', 'usergroups/' , + 'noderange/', 'nodes/', 'nodegroups/', 'storage/', 'usergroups/' , 'users/', 'uuid', 'version', 'staging/']