From 820d255a1ab7a08aca2799460946365736fc1de4 Mon Sep 17 00:00:00 2001 From: tkucherera Date: Mon, 2 Oct 2023 11:23:23 -0400 Subject: [PATCH 01/53] 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 b3f32eb8056fccec73d5b2889c240b5ddb62ad1b Mon Sep 17 00:00:00 2001 From: tkucherera Date: Fri, 6 Oct 2023 08:32:47 -0400 Subject: [PATCH 02/53] "firmware update on the server side" --- confluent_server/confluent/httpapi.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/confluent_server/confluent/httpapi.py b/confluent_server/confluent/httpapi.py index baf40606..2386d90d 100644 --- a/confluent_server/confluent/httpapi.py +++ b/confluent_server/confluent/httpapi.py @@ -888,6 +888,28 @@ def resourcehandler_backend(env, start_response): yield rsp return + elif (operation == 'create' and ('/firmware/updates/active' in env['PATH_INFO'])): + url = env['PATH_INFO'] + if 'application/json' in reqtype: + if not isinstance(reqbody, str): + reqbody = reqbody.decode('utf8') + pbody = json.loads(reqbody) + args = pbody['args'] + file_directory = '/var/lib/confluent/client_assets/{}'.format(args.split('/')[-1]) + filepath = '{0}/{1}'.format(file_directory, os.listdir(file_directory)[0]) # TODO find a way to validate that the file is found and its the expected one + args_dict = {'filename': filepath} + noderrs = {} + nodeurls = {} + hdlr = pluginapi.handle_path(url, operation, cfgmgr, args_dict) + for res in hdlr: + if isinstance(res, confluent.messages.CreatedResource): + watchurl = res.kvpairs['created'] + currnode = watchurl.split('/')[1] + nodeurls[currnode] = '/' + watchurl + yield json.dumps({'data': nodeurls}) + start_response('200 OK', headers) + return + elif (operation == 'create' and ('/staging' in env['PATH_INFO'])): url = env['PATH_INFO'] args_dict = {} From c678510b0256069c76c523035dc2b6e9948c900f Mon Sep 17 00:00:00 2001 From: tkucherera Date: Tue, 25 Jun 2024 14:37:10 -0400 Subject: [PATCH 03/53] working webauthn backend --- confluent_server/confluent/webauthn.py | 445 ++++++++++++++++++++----- 1 file changed, 369 insertions(+), 76 deletions(-) diff --git a/confluent_server/confluent/webauthn.py b/confluent_server/confluent/webauthn.py index 7e3b148f..a07b57f1 100644 --- a/confluent_server/confluent/webauthn.py +++ b/confluent_server/confluent/webauthn.py @@ -1,109 +1,399 @@ -import base64 +from webauthn_rp.registrars import CredentialData import confluent.tlvdata as tlvdata import confluent.util as util import json -import pywarp -import pywarp.backends -import pywarp.credentials + + +import secrets, time +from typing import Any, Optional +from webauthn_rp.backends import CredentialsBackend +from webauthn_rp.builders import * +from webauthn_rp.converters import cose_key, jsonify +from webauthn_rp.errors import WebAuthnRPError +from webauthn_rp.parsers import parse_cose_key, parse_public_key_credential +from webauthn_rp.registrars import * +from webauthn_rp.types import ( + AttestationObject, AttestationType, AuthenticatorAssertionResponse, + AuthenticatorAttestationResponse, AuthenticatorData, + COSEAlgorithmIdentifier, PublicKeyCredential, + PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, + PublicKeyCredentialRpEntity, PublicKeyCredentialType, + PublicKeyCredentialUserEntity, TrustedPath) + challenges = {} -class ConfluentBackend(pywarp.backends.CredentialStorageBackend): - def __init__(self, cfg): - self.cfg = cfg +CONFIG_MANAGER = None - def get_credential_ids_by_email(self, email): - if not isinstance(email, str): - email = email.decode('utf8') - authenticators = self.cfg.get_user(email).get('authenticators', {}) - if not authenticators: - raise Exception('No authenticators found') - for cid in authenticators: - yield base64.b64decode(cid) +class Credential(): + def __init__(self, id, signature_count, public_key): + self.id = id + self.signature_count = signature_count + self.credential_public_key = public_key - def get_credential_by_email_id(self, email, id): - if not isinstance(email, str): - email = email.decode('utf8') - authenticators = self.cfg.get_user(email).get('authenticators', {}) - cid = base64.b64encode(id).decode('utf8') - pk = authenticators[cid]['cpk'] - pk = base64.b64decode(pk) - return pywarp.credentials.Credential(credential_id=id, credential_public_key=pk) +class Challenge(): + def __init__(self, request, timstamp_ms, id=None) -> None: + if id is None: + self.id = util.randomstring(16) + else: + self.id = id + self.request = request + self.timestamp_ms = timstamp_ms - def get_credential_by_email(self, email): - if not isinstance(email, str): - email = email.decode('utf8') - authenticators = self.cfg.get_user(email) - cid = list(authenticators)[0] - cred = authenticators[cid] - cid = base64.b64decode(cred['cid']) - cpk = base64.b64decode(cred['cpk']) - return pywarp.credentials.Credential(credential_id=cid, credential_public_key=cpk) - def save_credential_for_user(self, email, credential): - if not isinstance(email, str): - email = email.decode('utf8') - cid = base64.b64encode(credential.id).decode('utf8') - credential = {'cid': cid, 'cpk': base64.b64encode(bytes(credential.public_key)).decode('utf8')} - authenticators = self.cfg.get_user(email).get('authenticators', {}) - authenticators[cid] = credential - self.cfg.set_user(email, {'authenticators': authenticators}) - def save_challenge_for_user(self, email, challenge, type): - if not isinstance(email, str): - email = email.decode('utf8') - challenges[email] = challenge +class User(): + def __init__(self, id, username, user_handle, challenge: Challenge = None, credential: Credential = None): + self.id = id + self.username = username + self.user_handle = user_handle + self.challenges = challenge + self.credentials = credential - def get_challenge_for_user(self, email, type): - if not isinstance(email, str): - email = email.decode('utf8') - return challenges[email] + def __parse_credentials(self): + return {"id": self.credentials.id, "signature_count": self.credentials.signature_count, "credential_public_key": self.credentials.credential_public_key} + def __parse_challenges(self): + return {"id": self.challenges.id, 'request': self.challenges.request, 'timestamp_ms': self.challenges.timestamp_ms} + + + @staticmethod + def seek_credential_by_id(credential_id): + """ + There certainly is a better way to do this but for now lets try the wrong way that works + """ + for username in CONFIG_MANAGER.list_users(): + authenticators = CONFIG_MANAGER.get_user(username).get('authenticators', {}) + try: + credential = authenticators['credentials'] + except KeyError: + continue + if "id" in credential.keys() and credential["id"] == credential_id: + #for now leaving signature count as None + return (Credential(id=credential["id"], signature_count=None, public_key=credential["credential_public_key"]), username) + return None + + + + @staticmethod + def get_credential(credential_id, username): + if not isinstance(username, str): + username = username.decode('utf8') + authenticators = CONFIG_MANAGER.get_user(username).get('authenticators', {}) + try: + credential = authenticators['credentials'] + except KeyError: + return None + if credential_id is None: + return Credential(id=credential["id"], signature_count=credential["signature_count"], public_key=credential["credential_public_key"]) + if credential["id"] == credential_id: + return Credential(id=credential["id"], signature_count=credential["signature_count"], public_key=credential["credential_public_key"]) + + return None + + @staticmethod + def get_challenge(challengeID, username): + if not isinstance(username, str): + username = username.decode('utf8') + authenticators = CONFIG_MANAGER.get_user(username).get('authenticators', {}) + challenge = authenticators['challenges'] + if challenge["id"] == challengeID: + return Challenge(request=challenge["request"], timstamp_ms=challenge["timestamp_ms"], id=challenge["id"]) + + return None + + @staticmethod + def get(username): + if not CONFIG_MANAGER: + raise Exception('config manager is not set up') + if not isinstance(username, str): + username = username.decode('utf8') + userinfo = CONFIG_MANAGER.get_user(username) + authenticators = CONFIG_MANAGER.get_user(username).get('authenticators', {}) + if userinfo is None: + return None + authid = userinfo.get('webauthid', None) + challenge = authenticators.get("challenges", None) + challenges_return = Challenge(challenge['request'], challenge['timestamp_ms'], id=challenge["id"]) + + credential = authenticators.get("credentials", None) + credentials_return = (Credential(credential['id'], credential['signature_count'], credential["credential_public_key"])) + + return User(id=None, username=username, user_handle=authid, challenge=challenges_return, credential=credentials_return) + + def save(self): + authenticators = CONFIG_MANAGER.get_user(self.username).get('authenticators', {}) + authenticators['challenges'] = self.__parse_challenges() # Looks like the bigger the array we encounter problems changing to just save one challenge + authenticators['credentials'] = self.__parse_credentials() + + CONFIG_MANAGER.set_user(self.username, {'authenticators': authenticators}) + + + def add(self, item): + if isinstance(item, Challenge): + self.challenges = item + elif isinstance(item, Credential): + self.credentials = item + + def update(self, item): + if isinstance(item, Challenge): + self.challenges = item + elif isinstance(item, Credential): + self.credentials = item + return + #raise Exception("Credential item not found") + + +def timestamp_ms(): + return int(time.time() * 1000) + + +class RegistrarImpl(CredentialsRegistrar): + def register_credential_attestation( + self, + credential: PublicKeyCredential, + att: AttestationObject, + att_type: AttestationType, + user: PublicKeyCredentialUserEntity, + rp: PublicKeyCredentialRpEntity, + trusted_path: Optional[TrustedPath] = None) -> Any: + + assert att.auth_data is not None + assert att.auth_data.attested_credential_data is not None + cpk = att.auth_data.attested_credential_data.credential_public_key + + user_model = User.get(user.name) + if user_model is None: + return 'No user found' + + credential_model = Credential(id=credential.raw_id, signature_count=None, public_key=cose_key(cpk)) + user_model.add(credential_model) + user_model.save() + + def register_credential_assertion( + self, + credential: PublicKeyCredential, + authenticator_data: AuthenticatorData, + user: PublicKeyCredentialUserEntity, + rp: PublicKeyCredentialRpEntity) -> Any: + + user_model = User.get(user.name) + credential_model = User.get_credential(credential_id=credential.raw_id, username=user.name) + credential_model.signature_count = None + user_model.update(credential_model) + user_model.save() + + def get_credential_data( + self, + credential_id: bytes) -> Optional[CredentialData]: + + #credential_model = User.get_credential(credential_id=credential_id, username=username) + (credential_model, username) = User.seek_credential_by_id(credential_id) + user_model = User.get(username) + + return CredentialData( + parse_cose_key(credential_model.credential_public_key), + credential_model.signature_count, + PublicKeyCredentialUserEntity( + name=user_model.username, + id=user_model.user_handle, + display_name=user_model.username + ) + ) + + +APP_ORIGIN = 'https://ndiamai' +APP_TIMEOUT = 60000 +APP_RELYING_PARTY = PublicKeyCredentialRpEntity(name='Confluent Web UI', id="ndiamai") + +APP_CCO_BUILDER = CredentialCreationOptionsBuilder( + rp=APP_RELYING_PARTY, + pub_key_cred_params=[ + PublicKeyCredentialParameters(type=PublicKeyCredentialType.PUBLIC_KEY, + alg=COSEAlgorithmIdentifier.Value.ES256) + ], + timeout=APP_TIMEOUT, +) + +APP_CRO_BUILDER = CredentialRequestOptionsBuilder( + rp_id=APP_RELYING_PARTY.id, + timeout=APP_TIMEOUT, +) + +APP_CREDENTIALS_BACKEND = CredentialsBackend(RegistrarImpl()) + +def registration_request(username, cfg): + user_model = User.get(username) + if user_model is None: + raise Exception("User not foud") + + challenge_bytes = secrets.token_bytes(64) + challenge = Challenge(request=challenge_bytes, timstamp_ms=timestamp_ms()) + user_model.add(challenge) + user_model.save() + + options = APP_CCO_BUILDER.build( + user=PublicKeyCredentialUserEntity( + name=username, + id=user_model.user_handle, + display_name=username + ), + challenge=challenge_bytes + ) + + options_json = jsonify(options) + return { + 'challengeID': challenge.id, + 'creationOptions': options_json + } + +def registration_response(request, username): + try: + challengeID = request["challengeID"] + credential = parse_public_key_credential(json.loads(request["credential"])) + except Exception: + raise Exception("Could not parse input data") + + if type(credential.response) is not AuthenticatorAttestationResponse: + raise Exception("Invalid response type") + + challenge_model = User.get_challenge(challengeID, username) + if not challenge_model: + raise Exception("Could not find challenge matching given id") + + user_model = User.get(username) + if not user_model: + raise Exception("Invalid Username") + + current_timestamp = timestamp_ms() + if current_timestamp - challenge_model.timestamp_ms > APP_TIMEOUT: + return "Timeout" + + + user_entity = PublicKeyCredentialUserEntity(name=user_model.username, id=user_model.user_handle, display_name=user_model.username) + try: + APP_CREDENTIALS_BACKEND.handle_credential_attestation( + credential=credential, + user=user_entity, + rp=APP_RELYING_PARTY, + expected_challenge=challenge_model.request, + expected_origin=APP_ORIGIN + ) + except WebAuthnRPError: + raise Exception("Could not handle credential attestation") + + return True + + +def authentication_request(username): + user_model = User.get(username) + + if user_model is None: + return 'User not registered' + + credential = user_model.get_credential(None, username) + print(credential) + if credential is None: + return f'No credential for User found {username}' + + challenge_bytes = secrets.token_bytes(64) + challenge = Challenge(request=challenge_bytes, timstamp_ms=timestamp_ms()) + user_model.add(challenge) + user_model.save() + + options = APP_CRO_BUILDER.build( + challenge=challenge_bytes, + allow_credentials=[ + PublicKeyCredentialDescriptor( + id=credential.id, + type=PublicKeyCredentialType.PUBLIC_KEY + ) + ] + ) + + options_json = jsonify(options) + return { + 'challengeID': challenge.id, + 'requestOptions': options_json + } + +def authentication_response(request, username): + try: + challengeID = request["challengeID"] + credential = parse_public_key_credential(json.loads(request["credential"])) + except Exception: + raise Exception("Could not parse input data") + + if type(credential.response) is not AuthenticatorAssertionResponse: + raise Exception('Invalid response type') + + challenge_model = User.get_challenge(challengeID, username) + if not challenge_model: + raise Exception("Could not find challenge matching given id") + + user_model = User.get(username) + if not user_model: + raise Exception("Invalid Username") + + current_timestamp = timestamp_ms() + if current_timestamp - challenge_model.timestamp_ms > APP_TIMEOUT: + return "Timeout" + + user_entity = PublicKeyCredentialUserEntity(name=user_model.username, id=user_model.user_handle, display_name=user_model.username) + + try: + APP_CREDENTIALS_BACKEND.handle_credential_assertion( + credential=credential, + user=user_entity, + rp=APP_RELYING_PARTY, + expected_challenge=challenge_model.request, + expected_origin=APP_ORIGIN + ) + except WebAuthnRPError: + raise Exception('Could not handle credential assertion') + + return {"verified": True} + + + def handle_api_request(url, env, start_response, username, cfm, headers, reqbody, authorized): + """ + For now webauth is going to be limited to just one passkey per user + If you try to register a new passkey this will just clear the old one and regist the new passkey + """ + global CONFIG_MANAGER + CONFIG_MANAGER = cfm if env['REQUEST_METHOD'] != 'POST': raise Exception('Only POST supported for webauthn operations') url = url.replace('/sessions/current/webauthn', '') if url == '/registration_options': - rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=ConfluentBackend(cfm), require_attestation=False) userinfo = cfm.get_user(username) if not userinfo: cfm.create_user(username, role='Stub') userinfo = cfm.get_user(username) - authid = userinfo.get('authid', None) + authid = userinfo.get('webauthid', None) if not authid: - authid = util.randomstring(64) - cfm.set_user(username, {'authid': authid}) - opts = rp.get_registration_options(username) - # pywarp generates an id derived - # from username, which is a 'must not' in the spec - # we replace that with a complying approach - opts['user']['id'] = authid - if 'icon' in opts['user']: - del opts['user']['icon'] - if 'id' in opts['rp']: - del opts['rp']['id'] + authid = secrets.token_bytes(64) + cfm.set_user(username, {'webauthid': authid}) + opts = registration_request(username, cfm) start_response('200 OK', headers) yield json.dumps(opts) elif url.startswith('/registered_credentials/'): username = url.rsplit('/', 1)[-1] - rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=ConfluentBackend(cfm)) + userinfo = cfm.get_user(username) if not isinstance(username, bytes): username = username.encode('utf8') - opts = rp.get_authentication_options(username) - opts['challenge'] = base64.b64encode(opts['challenge']).decode('utf8') + opts = authentication_request(username) start_response('200 OK', headers) yield json.dumps(opts) elif url.startswith('/validate/'): username = url.rsplit('/', 1)[-1] + userinfo = cfm.get_user(username) if not isinstance(username, bytes): username = username.encode('utf8') - rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=ConfluentBackend(cfm)) req = json.loads(reqbody) - for x in req: - req[x] = base64.b64decode(req[x].replace('-', '+').replace('_', '/')) - req['email'] = username - rsp = rp.verify(**req) + rsp = authentication_response(req, username) if start_response: start_response('200 OK', headers) sessinfo = {'username': username} @@ -116,13 +406,16 @@ def handle_api_request(url, env, start_response, username, cfm, headers, reqbody else: yield rsp elif url == '/register_credential': - rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=ConfluentBackend(cfm), require_attestation=False) req = json.loads(reqbody) - for x in req: - req[x] = base64.b64decode(req[x].replace('-', '+').replace('_', '/')) + userinfo = cfm.get_user(username) if not isinstance(username, bytes): username = username.encode('utf8') - req['email'] = username - rsp = rp.register(**req) - start_response('200 OK', headers) - yield json.dumps(rsp) \ No newline at end of file + rsp = registration_response(req, username) + if rsp == 'Timeout': + start_response('408 Timeout', headers) + else: + print('worked out') + start_response('200 OK', headers) + yield json.dumps({'status': 'Success'}) + + From fa940579f1a16786f6b6873eca629fbc0ff1cf38 Mon Sep 17 00:00:00 2001 From: tkucherera Date: Thu, 25 Jul 2024 14:07:27 -0400 Subject: [PATCH 04/53] adding webathn-rp dependency --- confluent_server/confluent_server.spec.tmpl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/confluent_server/confluent_server.spec.tmpl b/confluent_server/confluent_server.spec.tmpl index bf81c969..af4a457f 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 +Requires: python-pyghmi >= 1.0.34, python-eventlet, python-greenlet, python-pycryptodomex >= 3.4.7, confluent_client == %{version}, python-pyparsing, python-paramiko, python-webauthn-rp, 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 %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-webauthn-rp, 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 %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-webauthn-rp, 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 %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.0.34, python3-eventlet, python3-greenlet, python3-pycryptodome >= 3.4.7, confluent_client == %{version}, python3-pyparsing, python3-paramiko, python3-webauthn-rp, 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 8704afcee5f0aa7f38f7923cdfe82120b536c380 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 10 Sep 2024 11:28:00 -0400 Subject: [PATCH 05/53] 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 06/53] 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 db381d377b27b39525227f9920b838117b7f0738 Mon Sep 17 00:00:00 2001 From: tkucherera Date: Thu, 12 Sep 2024 10:10:53 -0400 Subject: [PATCH 07/53] account for timeout --- confluent_server/confluent/webauthn.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/confluent_server/confluent/webauthn.py b/confluent_server/confluent/webauthn.py index a07b57f1..c40ea0c4 100644 --- a/confluent_server/confluent/webauthn.py +++ b/confluent_server/confluent/webauthn.py @@ -394,7 +394,9 @@ def handle_api_request(url, env, start_response, username, cfm, headers, reqbody username = username.encode('utf8') req = json.loads(reqbody) rsp = authentication_response(req, username) - if start_response: + if rsp == 'Timeout': + start_response('408 Timeout', headers) + elif rsp['verified'] and start_response: start_response('200 OK', headers) sessinfo = {'username': username} if 'authtoken' in authorized: From d553ab864bbcc3da82db8a316b825aa76b133014 Mon Sep 17 00:00:00 2001 From: tkucherera Date: Thu, 12 Sep 2024 10:27:05 -0400 Subject: [PATCH 08/53] 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/'] From dbfb800c1b3755b183fd14a32525023350520016 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 12 Sep 2024 13:26:15 -0400 Subject: [PATCH 09/53] Fix regression in pyghmi dependency version --- confluent_server/confluent_server.spec.tmpl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/confluent_server/confluent_server.spec.tmpl b/confluent_server/confluent_server.spec.tmpl index a13fd104..2d3af243 100644 --- a/confluent_server/confluent_server.spec.tmpl +++ b/confluent_server/confluent_server.spec.tmpl @@ -19,15 +19,15 @@ 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-webauthn-rp, 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.5.71, python-eventlet, python-greenlet, python-pycryptodomex >= 3.4.7, confluent_client == %{version}, python-pyparsing, python-paramiko, python-webauthn-rp, 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 %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-webauthn-rp, 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.5.71, python3-eventlet, python3-greenlet, python3-pycryptodomex >= 3.4.7, confluent_client == %{version}, python3-pyparsing, python3-paramiko, python3-webauthn-rp, 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 %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-webauthn-rp, 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.5.71, python3-eventlet, python3-greenlet, python3-pycryptodomex >= 3.4.7, confluent_client == %{version}, python3-pyparsing, python3-paramiko, python3-dns, python3-webauthn-rp, 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 %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-webauthn-rp, 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-webauthn-rp, 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 From 9bce0de93d1a3d150293e7b028b3910dd0d66e35 Mon Sep 17 00:00:00 2001 From: tkucherera Date: Wed, 18 Sep 2024 16:22:09 -0400 Subject: [PATCH 10/53] fix minor bugs and code clean up --- confluent_server/confluent/webauthn.py | 27 +++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/confluent_server/confluent/webauthn.py b/confluent_server/confluent/webauthn.py index c40ea0c4..efecdbc1 100644 --- a/confluent_server/confluent/webauthn.py +++ b/confluent_server/confluent/webauthn.py @@ -51,11 +51,13 @@ class User(): self.credentials = credential def __parse_credentials(self): - return {"id": self.credentials.id, "signature_count": self.credentials.signature_count, "credential_public_key": self.credentials.credential_public_key} + if self.credentials: + return {"id": self.credentials.id, "signature_count": self.credentials.signature_count, "credential_public_key": self.credentials.credential_public_key} def __parse_challenges(self): - return {"id": self.challenges.id, 'request': self.challenges.request, 'timestamp_ms': self.challenges.timestamp_ms} + if self.challenges: + return {"id": self.challenges.id, 'request': self.challenges.request, 'timestamp_ms': self.challenges.timestamp_ms} @staticmethod @@ -81,10 +83,10 @@ class User(): if not isinstance(username, str): username = username.decode('utf8') authenticators = CONFIG_MANAGER.get_user(username).get('authenticators', {}) - try: - credential = authenticators['credentials'] - except KeyError: - return None + credential = authenticators.get('credentials', None) + if credential is None: + return None + if credential_id is None: return Credential(id=credential["id"], signature_count=credential["signature_count"], public_key=credential["credential_public_key"]) if credential["id"] == credential_id: @@ -105,6 +107,8 @@ class User(): @staticmethod def get(username): + challenges_return = None + credentials_return = None if not CONFIG_MANAGER: raise Exception('config manager is not set up') if not isinstance(username, str): @@ -115,10 +119,12 @@ class User(): return None authid = userinfo.get('webauthid', None) challenge = authenticators.get("challenges", None) - challenges_return = Challenge(challenge['request'], challenge['timestamp_ms'], id=challenge["id"]) - + if challenge: + challenges_return = Challenge(challenge['request'], challenge['timestamp_ms'], id=challenge["id"]) + credential = authenticators.get("credentials", None) - credentials_return = (Credential(credential['id'], credential['signature_count'], credential["credential_public_key"])) + if credential: + credentials_return = (Credential(credential['id'], credential['signature_count'], credential["credential_public_key"])) return User(id=None, username=username, user_handle=authid, challenge=challenges_return, credential=credentials_return) @@ -293,9 +299,8 @@ def authentication_request(username): return 'User not registered' credential = user_model.get_credential(None, username) - print(credential) if credential is None: - return f'No credential for User found {username}' + return 'No credential found' challenge_bytes = secrets.token_bytes(64) challenge = Challenge(request=challenge_bytes, timstamp_ms=timestamp_ms()) From f19234419dbf28682e2da23aebeb8f52a5f6dad2 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 19 Sep 2024 13:15:10 -0400 Subject: [PATCH 11/53] Implement non-root ssh for SUSE diskless --- .../profiles/default/scripts/imageboot.sh | 1 + imgutil/imgutil | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/confluent_osdeploy/suse15-diskless/profiles/default/scripts/imageboot.sh b/confluent_osdeploy/suse15-diskless/profiles/default/scripts/imageboot.sh index 91e62ebb..1fb4e6a2 100644 --- a/confluent_osdeploy/suse15-diskless/profiles/default/scripts/imageboot.sh +++ b/confluent_osdeploy/suse15-diskless/profiles/default/scripts/imageboot.sh @@ -140,4 +140,5 @@ mv /lib/modules/$(uname -r) /lib/modules/$(uname -r)-ramfs ln -s /sysroot/lib/modules/$(uname -r) /lib/modules/ mv /lib/firmware /lib/firmware-ramfs ln -s /sysroot/lib/firmware /lib/firmware +chroot /sysroot chkstat --system --set --noheader > /dev/null exec /opt/confluent/bin/start_root diff --git a/imgutil/imgutil b/imgutil/imgutil index c5446069..276ff601 100644 --- a/imgutil/imgutil +++ b/imgutil/imgutil @@ -612,6 +612,22 @@ class SuseHandler(OsHandler): else: subprocess.check_call(['zypper', '-n', '-R', self.targpath, 'install'] + self.zyppargs) os.symlink('/usr/lib/systemd/system/sshd.service', os.path.join(self.targpath, 'etc/systemd/system/multi-user.target.wants/sshd.service')) + with open(os.path.join(self.targpath, 'etc/permissions.local'), 'a') as permout: + permout.write( + '/usr/lib/ssh/ssh-keysign root:ssh_keys 2711\n' + '/etc/ssh/ssh_host_dsa_key root:ssh_keys 640\n' + '/etc/ssh/ssh_host_ecdsa_key root:ssh_keys 640\n' + '/etc/ssh/ssh_host_ed25519_key root:ssh_keys 640\n' + '/etc/ssh/ssh_host_rsa_key root:ssh_keys 640\n' + ) + args.cmd = ['groupadd', 'ssh_keys'] + run_constrainedx(fancy_chroot, (args, + self.targpath)) + args.cmd = ['chkstat', '--system', '--set'], + run_constrainedx(fancy_chroot, (args, + self.targpath)) + + if os.path.exists(os.path.join(self.targpath, 'sbin/mkinitrd')): args.cmd = ['mkinitrd'] else: From 936153490a9830adba89bf582b6b9a0e1a6abccb Mon Sep 17 00:00:00 2001 From: tkucherera Date: Thu, 19 Sep 2024 16:21:39 -0400 Subject: [PATCH 12/53] remove hardcorded values --- confluent_server/confluent/webauthn.py | 52 +++++++++++++++----------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/confluent_server/confluent/webauthn.py b/confluent_server/confluent/webauthn.py index efecdbc1..b9699f5a 100644 --- a/confluent_server/confluent/webauthn.py +++ b/confluent_server/confluent/webauthn.py @@ -209,27 +209,27 @@ class RegistrarImpl(CredentialsRegistrar): ) -APP_ORIGIN = 'https://ndiamai' -APP_TIMEOUT = 60000 -APP_RELYING_PARTY = PublicKeyCredentialRpEntity(name='Confluent Web UI', id="ndiamai") -APP_CCO_BUILDER = CredentialCreationOptionsBuilder( +APP_TIMEOUT = 60000 + + + + + + +APP_CREDENTIALS_BACKEND = CredentialsBackend(RegistrarImpl()) + +def registration_request(username, cfg, APP_RELYING_PARTY): + + APP_CCO_BUILDER = CredentialCreationOptionsBuilder( rp=APP_RELYING_PARTY, pub_key_cred_params=[ PublicKeyCredentialParameters(type=PublicKeyCredentialType.PUBLIC_KEY, alg=COSEAlgorithmIdentifier.Value.ES256) ], timeout=APP_TIMEOUT, -) + ) -APP_CRO_BUILDER = CredentialRequestOptionsBuilder( - rp_id=APP_RELYING_PARTY.id, - timeout=APP_TIMEOUT, -) - -APP_CREDENTIALS_BACKEND = CredentialsBackend(RegistrarImpl()) - -def registration_request(username, cfg): user_model = User.get(username) if user_model is None: raise Exception("User not foud") @@ -254,7 +254,7 @@ def registration_request(username, cfg): 'creationOptions': options_json } -def registration_response(request, username): +def registration_response(request, username, APP_RELYING_PARTY, APP_ORIGIN): try: challengeID = request["challengeID"] credential = parse_public_key_credential(json.loads(request["credential"])) @@ -292,7 +292,12 @@ def registration_response(request, username): return True -def authentication_request(username): +def authentication_request(username, APP_RELYING_PARTY): + APP_CRO_BUILDER = CredentialRequestOptionsBuilder( + rp_id=APP_RELYING_PARTY.id, + timeout=APP_TIMEOUT, + ) + user_model = User.get(username) if user_model is None: @@ -323,7 +328,7 @@ def authentication_request(username): 'requestOptions': options_json } -def authentication_response(request, username): +def authentication_response(request, username, APP_RELYING_PARTY, APP_ORIGIN): try: challengeID = request["challengeID"] credential = parse_public_key_credential(json.loads(request["credential"])) @@ -365,10 +370,15 @@ def authentication_response(request, username): def handle_api_request(url, env, start_response, username, cfm, headers, reqbody, authorized): """ For now webauth is going to be limited to just one passkey per user - If you try to register a new passkey this will just clear the old one and regist the new passkey + If you try to register a new passkey this will just clear the old one and register the new passkey """ global CONFIG_MANAGER CONFIG_MANAGER = cfm + + APP_ORIGIN = 'https://' + env['HTTP_X_FORWARDED_HOST'] + HOST = env['HTTP_X_FORWARDED_HOST'] + APP_RELYING_PARTY = PublicKeyCredentialRpEntity(name='Confluent Web UI', id=HOST) + if env['REQUEST_METHOD'] != 'POST': raise Exception('Only POST supported for webauthn operations') url = url.replace('/sessions/current/webauthn', '') @@ -381,7 +391,7 @@ def handle_api_request(url, env, start_response, username, cfm, headers, reqbody if not authid: authid = secrets.token_bytes(64) cfm.set_user(username, {'webauthid': authid}) - opts = registration_request(username, cfm) + opts = registration_request(username, cfm, APP_RELYING_PARTY) start_response('200 OK', headers) yield json.dumps(opts) elif url.startswith('/registered_credentials/'): @@ -389,7 +399,7 @@ def handle_api_request(url, env, start_response, username, cfm, headers, reqbody userinfo = cfm.get_user(username) if not isinstance(username, bytes): username = username.encode('utf8') - opts = authentication_request(username) + opts = authentication_request(username, APP_RELYING_PARTY) start_response('200 OK', headers) yield json.dumps(opts) elif url.startswith('/validate/'): @@ -398,7 +408,7 @@ def handle_api_request(url, env, start_response, username, cfm, headers, reqbody if not isinstance(username, bytes): username = username.encode('utf8') req = json.loads(reqbody) - rsp = authentication_response(req, username) + rsp = authentication_response(req, username, APP_RELYING_PARTY, APP_ORIGIN) if rsp == 'Timeout': start_response('408 Timeout', headers) elif rsp['verified'] and start_response: @@ -417,7 +427,7 @@ def handle_api_request(url, env, start_response, username, cfm, headers, reqbody userinfo = cfm.get_user(username) if not isinstance(username, bytes): username = username.encode('utf8') - rsp = registration_response(req, username) + rsp = registration_response(req, username, APP_RELYING_PARTY, APP_ORIGIN) if rsp == 'Timeout': start_response('408 Timeout', headers) else: From a8df3692b6296a426834090896ae4fe98fab02c3 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 20 Sep 2024 17:26:02 -0400 Subject: [PATCH 13/53] Persist passkeys as text confluentdbutil and collective would choke on the binary. Have the binary bits be safely converted to/from base64. --- confluent_server/confluent/webauthn.py | 55 +++++++++++++++++++++----- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/confluent_server/confluent/webauthn.py b/confluent_server/confluent/webauthn.py index b9699f5a..4920ac33 100644 --- a/confluent_server/confluent/webauthn.py +++ b/confluent_server/confluent/webauthn.py @@ -2,6 +2,8 @@ from webauthn_rp.registrars import CredentialData import confluent.tlvdata as tlvdata import confluent.util as util import json +import copy +import base64 import secrets, time @@ -42,6 +44,22 @@ class Challenge(): +def _load_credentials(creds): + if creds is None: + return None + ret = copy.deepcopy(creds) + ret['credential_public_key'] = base64.b64decode(creds['credential_public_key']) + ret['id'] = base64.b64decode(creds['id']) + return ret + +def _load_authenticators(authenticators): + ret = authenticators + if 'challenges' in ret: + ret['challenges']['request'] = base64.b64decode(ret['challenges']['request']) + if 'credentials' in ret: + ret['credentials'] = _load_credentials(ret['credentials']) + return ret + class User(): def __init__(self, id, username, user_handle, challenge: Challenge = None, credential: Credential = None): self.id = id @@ -52,12 +70,15 @@ class User(): def __parse_credentials(self): if self.credentials: - return {"id": self.credentials.id, "signature_count": self.credentials.signature_count, "credential_public_key": self.credentials.credential_public_key} + credid = base64.b64encode(self.credentials.id).decode() + pubkey = base64.b64encode(self.credentials.credential_public_key).decode() + return {"id": credid, "signature_count": self.credentials.signature_count, "credential_public_key": pubkey} def __parse_challenges(self): if self.challenges: - return {"id": self.challenges.id, 'request': self.challenges.request, 'timestamp_ms': self.challenges.timestamp_ms} + request = base64.b64encode(self.challenges.request).decode() + return {"id": self.challenges.id, 'request': request, 'timestamp_ms': self.challenges.timestamp_ms} @staticmethod @@ -67,8 +88,9 @@ class User(): """ for username in CONFIG_MANAGER.list_users(): authenticators = CONFIG_MANAGER.get_user(username).get('authenticators', {}) + authenticators = _load_authenticators(authenticators) try: - credential = authenticators['credentials'] + credential = authenticators['credentials'] except KeyError: continue if "id" in credential.keys() and credential["id"] == credential_id: @@ -83,7 +105,8 @@ class User(): if not isinstance(username, str): username = username.decode('utf8') authenticators = CONFIG_MANAGER.get_user(username).get('authenticators', {}) - credential = authenticators.get('credentials', None) + authenticators = _load_authenticators(authenticators) + credential = authenticators.get('credentials', None) if credential is None: return None @@ -98,7 +121,11 @@ class User(): def get_challenge(challengeID, username): if not isinstance(username, str): username = username.decode('utf8') - authenticators = CONFIG_MANAGER.get_user(username).get('authenticators', {}) + authuser = CONFIG_MANAGER.get_user(username) + if not authuser: + return None + authenticators = authuser.get('authenticators', {}) + authenticators = _load_authenticators(authenticators) challenge = authenticators['challenges'] if challenge["id"] == challengeID: return Challenge(request=challenge["request"], timstamp_ms=challenge["timestamp_ms"], id=challenge["id"]) @@ -114,10 +141,18 @@ class User(): if not isinstance(username, str): username = username.decode('utf8') userinfo = CONFIG_MANAGER.get_user(username) - authenticators = CONFIG_MANAGER.get_user(username).get('authenticators', {}) + try: + authenticators = CONFIG_MANAGER.get_user(username).get('authenticators', {}) + except AttributeError: + return None if userinfo is None: return None - authid = userinfo.get('webauthid', None) + authenticators = _load_authenticators(authenticators) + b64authid = userinfo.get('webauthid', None) + if b64authid is None: + authid = None + else: + authid = base64.b64decode(b64authid) challenge = authenticators.get("challenges", None) if challenge: challenges_return = Challenge(challenge['request'], challenge['timestamp_ms'], id=challenge["id"]) @@ -130,6 +165,7 @@ class User(): def save(self): authenticators = CONFIG_MANAGER.get_user(self.username).get('authenticators', {}) + authenticators = _load_authenticators(authenticators) authenticators['challenges'] = self.__parse_challenges() # Looks like the bigger the array we encounter problems changing to just save one challenge authenticators['credentials'] = self.__parse_credentials() @@ -286,7 +322,7 @@ def registration_response(request, username, APP_RELYING_PARTY, APP_ORIGIN): expected_challenge=challenge_model.request, expected_origin=APP_ORIGIN ) - except WebAuthnRPError: + except WebAuthnRPError as wrp: raise Exception("Could not handle credential attestation") return True @@ -390,7 +426,8 @@ def handle_api_request(url, env, start_response, username, cfm, headers, reqbody authid = userinfo.get('webauthid', None) if not authid: authid = secrets.token_bytes(64) - cfm.set_user(username, {'webauthid': authid}) + b64authid = base64.b64encode(authid).decode() + cfm.set_user(username, {'webauthid': b64authid}) opts = registration_request(username, cfm, APP_RELYING_PARTY) start_response('200 OK', headers) yield json.dumps(opts) From 71a83ac39c887a4b5be9c56d5cf8ea63152bbaf0 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 20 Sep 2024 18:34:02 -0400 Subject: [PATCH 14/53] Try for more DNS lookups Try to hit likely DNS names, or at least provide a means of manipulating /etc/hosts to induce a good domain for the default certificate SAN fields. Note putting the FQDN first in /etc/hosts will get the FQDN in the certificate. --- confluent_server/confluent/certutil.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/confluent_server/confluent/certutil.py b/confluent_server/confluent/certutil.py index 4ac67165..3b8e5ef5 100644 --- a/confluent_server/confluent/certutil.py +++ b/confluent_server/confluent/certutil.py @@ -218,11 +218,17 @@ def create_certificate(keyout=None, certout=None, csrout=None): subprocess.check_call( ['openssl', 'ecparam', '-name', 'secp384r1', '-genkey', '-out', keyout]) - san = ['IP:{0}'.format(x) for x in get_ip_addresses()] + ipaddrs = list(get_ip_addresses()) + san = ['IP:{0}'.format(x) for x in ipaddrs] # It is incorrect to put IP addresses as DNS type. However # there exists non-compliant clients that fail with them as IP - san.extend(['DNS:{0}'.format(x) for x in get_ip_addresses()]) - san.append('DNS:{0}'.format(shortname)) + # san.extend(['DNS:{0}'.format(x) for x in ipaddrs]) + dnsnames = set(ipaddrs) + dnsnames.add(shortname) + for currip in ipaddrs: + dnsnames.add(socket.getnameinfo((currip, 0), 0)[0]) + for currname in dnsnames: + san.append('DNS:{0}'.format(currname)) #san.append('DNS:{0}'.format(longname)) san = ','.join(san) sslcfg = get_openssl_conf_location() From ed2a8b6f9dd5a0e857a5e06ca1f7509173ebd38b Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 23 Sep 2024 10:45:31 -0400 Subject: [PATCH 15/53] Provide an example to name constrain a CA It's imperfect, and abetter procedure should be written up for the more security conscious. --- misc/makealtca.py | 103 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 misc/makealtca.py diff --git a/misc/makealtca.py b/misc/makealtca.py new file mode 100644 index 00000000..644cc9ad --- /dev/null +++ b/misc/makealtca.py @@ -0,0 +1,103 @@ +# This creates a locked down variant of a confluent CA + +# The confluent CA naturally doesn't have any name constraints, which +# is great for flexibility. + +# However, if actually being imported into a web browser, it is likely +# to want to limit the CA to apply to cluster resources rather +# than having a blank check to vouch for any and all subjects. + +# This particular approach causes a certificate authority that +# can be imported and vouch for the subset of certificates +# that were issued by the normal CA that match name constraints + +# Unfortunately, *ALL* names are checked, whether they are relevant +# to the conversation or not, so we reproduce the logic and the resultant +# CA is fragile to IP addresses. + +# The better approach would be to issue a separate, full name only certificate +# and have a simpler alt CA. The same CA can be used to sign both certificates, +# and still import the limited one + + +import subprocess +import socket +import confluent.certutil as certutil +import sys +def create_alt_ca(certout, permitdomains): + # This is to create a constrained variant of the existing authority + # this will allow a client browser to only trust it for select domains + # while the nodes can more broadly trust it (e.g. to vouch for IP addresses) + sslcfg = certutil.get_openssl_conf_location() + newcfg = '/etc/confluent/tls/ca-alt/openssl-alt.cfg' + subj = subprocess.check_output( + ['openssl', 'x509', '-subject', '-noout', '-in', '/etc/confluent/tls/ca/cacert.pem'] + ).decode().replace('subject=', '') + serial = subprocess.check_output( + ['openssl', 'x509', '-serial', '-noout', '-in', '/etc/confluent/tls/ca/cacert.pem'] + ).decode().replace('serial=', '') + with open('/etc/confluent/tls/ca-alt/serial', 'w') as srl: + srl.write(serial) + settings = { + 'dir': '/etc/confluent/tls/ca-alt', + 'certificate': '$dir/cacert.pem', + 'private_key': '$dir/private/cakey.pem', + 'countryName': 'optional', + 'stateOrProvinceName': 'optional', + 'organizationName': 'optional', + } + keyin = '/etc/confluent/tls/ca/private/cakey.pem' + csrin = '/etc/confluent/tls/ca/ca.csr' + + shortname = 'r3u20' + + + ipaddrs = list(certutil.get_ip_addresses()) + san = [] # 'IP:{0}'.format(x) for x in ipaddrs] + # It is incorrect to put IP addresses as DNS type. However + # there exists non-compliant clients that fail with them as IP + # san.extend(['DNS:{0}'.format(x) for x in ipaddrs]) + dnsnames = set(ipaddrs) + dnsnames.add(shortname) + for currip in ipaddrs: + dnsnames.add(socket.getnameinfo((currip, 0), 0)[0]) + for currname in dnsnames: + san.append('DNS:{0}'.format(currname)) + + + + if permitdomains[0] == '': + permitdomains = [] + nameconstraints = ['permitted;DNS:{}'.format(x) for x in permitdomains] + nameconstraints.extend(['permitted;{}'.format(x) for x in san]) + nameconstraints = ','.join(nameconstraints) + if nameconstraints: + nameconstraints = f'nameConstraints = critical,{nameconstraints}\n' + certutil.mkdirp('/etc/confluent/tls/ca-alt/newcerts') + with open('/etc/confluent/tls/ca-alt/index.txt', 'w') as idx: + pass + + with open(sslcfg, 'r') as cfgin: + with open(newcfg, 'w') as cfgfile: + for line in cfgin.readlines(): + cfg = line.split('#')[0] + if '=' in cfg: + key, val = cfg.split('=', 1) + for stg in settings: + if certutil.substitute_cfg(stg, key, val, settings[stg], cfgfile, line): + break + else: + cfgfile.write(line.strip() + '\n') + continue + cfgfile.write(line.strip() + '\n') + cfgfile.write(f'\n[CACert]\nbasicConstraints = CA:true\n{nameconstraints}[ca_confluent]\n') + subprocess.check_call( + ['openssl', 'ca', '-config', newcfg, '-batch', '-selfsign', + '-extensions', 'CACert', '-extfile', newcfg, + '-notext', '-startdate', + '19700101010101Z', '-enddate', '21000101010101Z', '-keyfile', + keyin, '-out', certout, '-in', csrin] + ) +if __name__ == '__main__': + create_alt_ca(sys.argv[1], sys.argv[2].split(',')) + From a3212d760306b1e0e5ee51e9013aef764b58a130 Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Thu, 26 Sep 2024 13:19:21 +0200 Subject: [PATCH 16/53] Fix nodesensors --skipnumberless help text --- confluent_client/bin/nodesensors | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_client/bin/nodesensors b/confluent_client/bin/nodesensors index 61d4d602..2222abfd 100755 --- a/confluent_client/bin/nodesensors +++ b/confluent_client/bin/nodesensors @@ -67,7 +67,7 @@ argparser.add_option('-n', '--numreadings', type='int', argparser.add_option('-c', '--csv', action='store_true', help='Output in CSV format') argparser.add_option('-s', '--skipnumberless', action='store_true', - help='Output in CSV format') + help='Do not show non-numeric sensors') (options, args) = argparser.parse_args() repeatmode = False if options.interval: From 3ad53a3aacf5112c564b16233f528b570b52176b Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 27 Sep 2024 15:30:59 -0400 Subject: [PATCH 17/53] Fix client file staging Skip read during httpapi, and inject sleep between file transfer chunks. --- confluent_server/confluent/core.py | 1 + confluent_server/confluent/httpapi.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/confluent_server/confluent/core.py b/confluent_server/confluent/core.py index 5ef9271b..528c3f21 100644 --- a/confluent_server/confluent/core.py +++ b/confluent_server/confluent/core.py @@ -1343,6 +1343,7 @@ def handle_staging(pathcomponents, operation, configmanager, inputdata): datachunk = filedata['wsgi.input'].read(min(chunk_size, remaining_length)) f.write(datachunk) remaining_length -= len(datachunk) + eventlet.sleep(0) yield msg.FileUploadProgress(progress) yield msg.FileUploadProgress(100) diff --git a/confluent_server/confluent/httpapi.py b/confluent_server/confluent/httpapi.py index 5b00f4a8..7a40a988 100644 --- a/confluent_server/confluent/httpapi.py +++ b/confluent_server/confluent/httpapi.py @@ -681,7 +681,7 @@ def resourcehandler_backend(env, start_response): start_response('302 Found', headers) yield '' return - if 'CONTENT_LENGTH' in env and int(env['CONTENT_LENGTH']) > 0: + if 'CONTENT_LENGTH' in env and int(env['CONTENT_LENGTH']) > 0 and not '/staging' in env['PATH_INFO']: reqbody = env['wsgi.input'].read(int(env['CONTENT_LENGTH'])) reqtype = env['CONTENT_TYPE'] operation = opmap.get(env['REQUEST_METHOD'], None) From 910af18a007b762e217f9fe980342e624bb80e36 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 30 Sep 2024 13:57:09 -0400 Subject: [PATCH 18/53] Fix http websockify --- confluent_server/confluent/httpapi.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/confluent_server/confluent/httpapi.py b/confluent_server/confluent/httpapi.py index 7a40a988..0378fe5b 100644 --- a/confluent_server/confluent/httpapi.py +++ b/confluent_server/confluent/httpapi.py @@ -445,6 +445,8 @@ def websockify_data(data): data = data.decode('utf8') except UnicodeDecodeError: data = data.decode('cp437') + except AttributeError: # already str + pass data = u' ' + data return data From 84c119ce3d10c04147f795da51db913dd4fec2dd Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 4 Oct 2024 09:19:19 -0400 Subject: [PATCH 19/53] Reduce mandatory newlines between textgroup output --- confluent_client/confluent/textgroup.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/confluent_client/confluent/textgroup.py b/confluent_client/confluent/textgroup.py index e2f0dc7f..2297c4e5 100644 --- a/confluent_client/confluent/textgroup.py +++ b/confluent_client/confluent/textgroup.py @@ -171,7 +171,9 @@ class GroupedData(object): self.byoutput[outdata]))) currout += '\n====================================\n' currout += outdata - currout += '\n\n' + if currout[-1] != '\n': + currout += '\n' + currout += '\n' output.write(currout) output.flush() @@ -211,7 +213,9 @@ class GroupedData(object): else: currout += '\n'.join(colordiff(modaloutput.split('\n'), outdata.split('\n'))) - currout += '\n\n' + if currout[-1] != '\n': + currout += '\n' + currout += '\n' if reverse: revoutput.append(currout) else: From 3a0218c4212b0ab74de0a4572494b728a3b5f852 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 7 Oct 2024 13:51:55 -0400 Subject: [PATCH 20/53] Simplify profile label outside of bootloader --- confluent_osdeploy/esxi7/profiles/hypervisor/profile.yaml | 2 +- confluent_server/confluent/osimage.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/confluent_osdeploy/esxi7/profiles/hypervisor/profile.yaml b/confluent_osdeploy/esxi7/profiles/hypervisor/profile.yaml index dc0d1a33..b9c84687 100644 --- a/confluent_osdeploy/esxi7/profiles/hypervisor/profile.yaml +++ b/confluent_osdeploy/esxi7/profiles/hypervisor/profile.yaml @@ -1,3 +1,3 @@ -label: Confluent installation of VMware ESXi %%VERSION%% Hypervisor +label: VMware ESXi %%VERSION%% Hypervisor ostype: esxi kernelargs: runweasel diff --git a/confluent_server/confluent/osimage.py b/confluent_server/confluent/osimage.py index 387922fa..8b9101ec 100644 --- a/confluent_server/confluent/osimage.py +++ b/confluent_server/confluent/osimage.py @@ -91,10 +91,13 @@ def update_boot_esxi(profiledir, profile, label): newbootcfg = '' efibootcfg = '' filesneeded = [] + localabel = label + if 'installation of' not in localabel: + localabel = 'Confluent installation of {}'.format(localabel) for cfgline in bootcfg: if cfgline.startswith('title='): - newbootcfg += 'title={0}\n'.format(label) - efibootcfg += 'title={0}\n'.format(label) + newbootcfg += 'title={0}\n'.format(localabel) + efibootcfg += 'title={0}\n'.format(localabel) elif cfgline.startswith('kernelopt='): newbootcfg += 'kernelopt={0}\n'.format(kernelargs) efibootcfg += 'kernelopt={0}\n'.format(kernelargs) From b05b36484b99825a39e328d8c28bfc566a8854cb Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 10 Oct 2024 12:52:28 -0400 Subject: [PATCH 21/53] Fix file staging in http api --- confluent_server/confluent/httpapi.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/confluent_server/confluent/httpapi.py b/confluent_server/confluent/httpapi.py index 0378fe5b..2cfbc117 100644 --- a/confluent_server/confluent/httpapi.py +++ b/confluent_server/confluent/httpapi.py @@ -45,6 +45,7 @@ import eventlet import eventlet.greenthread import greenlet import json +import os import socket import sys import traceback @@ -981,7 +982,9 @@ def resourcehandler_backend(env, start_response): 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): + elif len(url.split('/')) == 2: + reqbody = env['wsgi.input'].read(int(env['CONTENT_LENGTH'])) + reqtype = env['CONTENT_TYPE'] if not isinstance(reqbody, str): reqbody = reqbody.decode('utf8') pbody = json.loads(reqbody) From 81b57d5db6182c5ef4218f912247c939a587fb0f Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 10 Oct 2024 16:22:58 -0400 Subject: [PATCH 22/53] Avoid potential endless recursion If we are detaching, skip reattach in scenario that leads to infinite recursion. --- confluent_server/confluent/consoleserver.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/confluent_server/confluent/consoleserver.py b/confluent_server/confluent/consoleserver.py index aa05b9b7..6dba9b9c 100644 --- a/confluent_server/confluent/consoleserver.py +++ b/confluent_server/confluent/consoleserver.py @@ -705,10 +705,13 @@ class ProxyConsole(object): if not util.cert_matches(self.managerinfo['fingerprint'], remote.getpeercert(binary_form=True)): raise Exception('Invalid peer certificate') - except Exception: + except Exception as e: + if _tracelog: + _tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event, + event=log.Events.stacktrace) eventlet.sleep(3) if self.clisession: - self.clisession.detach() + self.clisession.detach(False) self.detachsession(None) return tlvdata.recv(remote) @@ -835,7 +838,7 @@ class ConsoleSession(object): self._evt = None self.reghdl = None - def detach(self): + def detach(self, reattach=True): """Handler for the console handler to detach so it can reattach, currently to facilitate changing from one collective.manager to another @@ -843,9 +846,10 @@ class ConsoleSession(object): :return: """ self.conshdl.detachsession(self) - self.connect_session() - self.conshdl.attachsession(self) - self.write = self.conshdl.write + if reattach: + self.connect_session() + self.conshdl.attachsession(self) + self.write = self.conshdl.write def got_data(self, data): """Receive data from console and buffer From 356041566811a7442d80f569f17d0621e89a74c3 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 16 Oct 2024 11:40:00 -0400 Subject: [PATCH 23/53] Update attribute inheritance on rename --- confluent_server/confluent/config/configmanager.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/confluent_server/confluent/config/configmanager.py b/confluent_server/confluent/config/configmanager.py index 7702b97d..fa4cbccc 100644 --- a/confluent_server/confluent/config/configmanager.py +++ b/confluent_server/confluent/config/configmanager.py @@ -2353,6 +2353,9 @@ class ConfigManager(object): lidx = self._cfgstore['nodes'][node]['groups'].index(name) self._cfgstore['nodes'][node]['groups'][lidx] = renamemap[name] _mark_dirtykey('nodes', node, self.tenant) + for node in self._cfgstore['nodegroups'][renamemap[name]].get('nodes', []): + self._node_removed_from_group(node, name, {}) + self._node_added_to_group(node, renamemap[name], {}) self._bg_sync_to_file() From b99e4c94a0d8955e2db3256a054e5ec806f1a809 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 17 Oct 2024 14:56:19 -0400 Subject: [PATCH 24/53] Add Enlogic PDU support --- .../plugins/hardwaremanagement/enlogic.py | 279 ++++++++++++++++++ 1 file changed, 279 insertions(+) create mode 100644 confluent_server/confluent/plugins/hardwaremanagement/enlogic.py diff --git a/confluent_server/confluent/plugins/hardwaremanagement/enlogic.py b/confluent_server/confluent/plugins/hardwaremanagement/enlogic.py new file mode 100644 index 00000000..196b79df --- /dev/null +++ b/confluent_server/confluent/plugins/hardwaremanagement/enlogic.py @@ -0,0 +1,279 @@ +# Copyright 2022 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 pyghmi.util.webclient as wc +import confluent.util as util +import confluent.messages as msg +import confluent.exceptions as exc +import eventlet.green.time as time +import eventlet +import eventlet.greenpool as greenpool + + + +def simplify_name(name): + return name.lower().replace(' ', '_').replace('/', '-').replace('_-_', '-') + + +pdupool = greenpool.GreenPool(128) + +_pduclients = {} + + +class EnlogicClient(object): + def __init__(self, pdu, configmanager): + self.node = pdu + self.configmanager = configmanager + self._token = None + self._wc = None + self.username = None + + @property + def token(self): + if not self._token: + self._token = self.login(self.configmanager) + return self._token + + @property + def wc(self): + if self._wc: + print("done cache") + return self._wc + print("loggin") + 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) + if not target: + target = self.node + target = target.split('/', 1)[0] + cv = util.TLSCertVerifier( + self.configmanager, self.node, 'pubkeys.tls_hardwaremanager' + ).verify_cert + self._wc = wc.SecureHTTPConnection(target, port=443, verifycallback=cv) + return self._wc + + def grab_json_response(self, url, body=None): + rsp, status = self.wc.grab_json_response_with_status(url, body) + if status == 401: + self._token = None + if body and 'cookie' in body: + body['cookie'] = self.token + rsp, status = self.wc.grab_json_response_with_status(url, body) + if status < 300: + return rsp + return {} + + def login(self, configmanager): + 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) + + if not isinstance(username, str): + username = username.decode('utf8') + if not isinstance(passwd, str): + passwd = passwd.decode('utf8') + if not username or not passwd: + raise Exception('Missing username or password') + self.username = username + rsp = self.wc.grab_json_response( + '/xhrlogin.jsp', + {'username': username, 'password': passwd, 'cookie': 0} + ) + print(repr(rsp)) + self.authtoken = rsp['cookie'] + self.wc.set_header('Authorization', self.authtoken) + return self.authtoken + + def logout(self): + if self._token: + self.wc.grab_json_response( + '/xhrlogout.jsp', + {'timeout': 0, 'cookie': self._token}, + ) + self._token = None + + def get_outlet(self, outlet): + rsp = self.grab_json_response( + '/xhroutgetgrid.jsp', { + 'cookie': self.token, + 'pduid': 1 + }) + outlets = rsp['outlet'] + for olet in outlets: + if olet['id'] == int(outlet): + state = "on" if olet['powstat'] == 1 else "off" + return state + + def set_outlet(self, outlet, state): + bitflags = 2**(int(outlet) - 1) + outlet1 = bitflags & (2**24-1) + outlet2 = bitflags >> 24 + if state == 'off': + state = 0 + elif state == 'on': + state = 1 + else: + raise Exception("Unrecognized state " + repr(state)) + request = { + 'cookie': self.token, + 'outlet1': outlet1, + 'outlet2': outlet2, + 'pduid': 1, + 'powstat': state + } + rsp = self.grab_json_response('/xhroutpowstatset.jsp', request) + + +_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' + justnames = True + if category in ('leds, fans', 'temperature'): + return + if justnames: + yield msg.ChildCollection('total_energy') + yield msg.ChildCollection('total_apparent_power') + yield msg.ChildCollection('total_real_power') + return + sn = _sensors_by_node.get(node, None) + if not sn or sn[1] < time.time(): + gc = get_client(node, configmanager) + adev = gc.grab_json_response('/energy_get', {'cookie': gc.token, 'end': 1, 'start': 1}) + _sensors_by_node[node] = (adev, time.time() + 1) + sn = _sensors_by_node.get(node, None) + if sn: + sn = sn[0] + readings = [ + { + 'name': 'Total Energy', + 'value': float(sn[0]['total_energy']) * 0.001, + 'units': 'kWh', + 'type': 'Energy', + }, + { + 'name': 'Total Real Power', + 'value': float(sn[0]['active_power']), + 'units': 'W', + 'type': 'Power', + }, + { + 'name': 'Total Apparent Power', + 'value': float(sn[0]['apparent_power']), + 'units': 'W', + 'type': 'Power', + }, + ] + yield msg.SensorReadings(readings, name=node) + return + +def get_client(node, configmanager): + if node not in _pduclients: + _pduclients[node] = EnlogicClient(node, configmanager) + return _pduclients[node] + +def get_outlet(element, node, configmanager): + gc = get_client(node, configmanager) + state = gc.get_outlet(element[-1]) + return msg.PowerState(node=node, state=state) + + +def read_firmware(node, configmanager): + gc = get_client(node, configmanager) + adev = gc.grab_json_response('/xhrgetuserlist.jsp') + myversion = adev[0]['fwver'] + yield msg.Firmware([{'PDU Firmware': {'version': myversion}}], node) + + +def read_inventory(element, node, configmanager): + _inventory = {} + inventory = {} + gc = get_client(node, configmanager) + adev = gc.grab_json_response('/sys_info_get', { + 'cookie': gc.token, 'pduid': 1 + }) + inventory['present'] = True + inventory['name'] = 'PDU' + info = {} + info['Serial Number'] = adev['pdu'][0]['serial_number'] + info['Product Name'] = adev['pdu'][0]['model'] + info['Model'] = adev['pdu'][0]['part_number'] + inventory['information'] = info + yield msg.KeyValueData({'inventory': [inventory]}, node) + +def retrieve(nodes, element, configmanager, inputdata): + + 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': + gp = greenpool.GreenPile(pdupool) + for node in nodes: + gp.spawn(read_sensors, element, node, configmanager) + for rsp in gp: + for datum in rsp: + yield datum + return + 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') + return + + +def update(nodes, element, configmanager, inputdata): + if 'outlets' not in element: + for node in nodes: + yield msg.ConfluentResourceUnavailable(node, 'Not implemented') + return + for node in nodes: + gc = get_client(node, configmanager) + newstate = inputdata.powerstate(node) + gc.set_outlet(element[-1], newstate) + eventlet.sleep(1) + for res in retrieve(nodes, element, configmanager, inputdata): + yield res From ab0f48f3514272725e074e15686e9cd0bccb71a2 Mon Sep 17 00:00:00 2001 From: tkucherera Date: Tue, 22 Oct 2024 12:05:23 -0400 Subject: [PATCH 25/53] enable ability to clean up assets --- confluent_server/confluent/core.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/confluent_server/confluent/core.py b/confluent_server/confluent/core.py index 528c3f21..48af7b70 100644 --- a/confluent_server/confluent/core.py +++ b/confluent_server/confluent/core.py @@ -72,6 +72,7 @@ import struct import sys import uuid import yaml +import shutil pluginmap = {} @@ -1308,8 +1309,14 @@ class Staging: file = None return False - def deldirectory(self): - pass + @staticmethod + def remove_directory(directory): + storage_folder = '/var/lib/confluent/client_assets/' + directory + if os.path.exists(storage_folder): + shutil.rmtree(storage_folder) + else: + raise FileNotFoundError + return directory def handle_staging(pathcomponents, operation, configmanager, inputdata): ''' @@ -1348,9 +1355,12 @@ def handle_staging(pathcomponents, operation, configmanager, inputdata): yield msg.FileUploadProgress(100) - elif operation == 'retrieve': - pass - return + elif operation == 'delete': + if len(pathcomponents) == 3: + asset = Staging.remove_directory(pathcomponents[2]) + yield msg.DeletedResource(asset) + else: + raise Exception("Invalid url") def handle_path(path, operation, configmanager, inputdata=None, autostrip=True): """Given a full path request, return an object. From a46bcfa2b5b588e476d4114647622c734dc5fc22 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 25 Oct 2024 09:52:10 -0400 Subject: [PATCH 26/53] Add CentOS Stream 10 and Alma Kitten 10 Similar to 9, but now hooks must be in /var instead of /usr --- confluent_osdeploy/confluent_osdeploy.spec.tmpl | 8 ++++++-- confluent_server/confluent/osimage.py | 15 ++++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/confluent_osdeploy/confluent_osdeploy.spec.tmpl b/confluent_osdeploy/confluent_osdeploy.spec.tmpl index 5faab31f..26beb74f 100644 --- a/confluent_osdeploy/confluent_osdeploy.spec.tmpl +++ b/confluent_osdeploy/confluent_osdeploy.spec.tmpl @@ -28,11 +28,15 @@ This contains support utilities for enabling deployment of x86_64 architecture s #cp start_root urlmount ../stateless-bin/ #cd .. ln -s el8 el9 -for os in rhvh4 el7 genesis el8 suse15 ubuntu18.04 ubuntu20.04 ubuntu22.04 ubuntu24.04 coreos el9; do +cp -a el8 el10 +mv el10/initramfs/usr el10/initramfs/var +for os in rhvh4 el7 genesis el8 suse15 ubuntu18.04 ubuntu20.04 ubuntu22.04 ubuntu24.04 coreos el9 el10; do mkdir ${os}out cd ${os}out if [ -d ../${os}bin ]; then cp -a ../${os}bin/opt . + elif [ $os = el10 ]; then + cp -a ../el9bin/opt . else cp -a ../el8bin/opt . fi @@ -78,7 +82,7 @@ cp -a esxi7 esxi8 %install mkdir -p %{buildroot}/opt/confluent/share/licenses/confluent_osdeploy/ cp LICENSE %{buildroot}/opt/confluent/share/licenses/confluent_osdeploy/ -for os in rhvh4 el7 el8 el9 genesis suse15 ubuntu20.04 ubuntu18.04 ubuntu22.04 ubuntu24.04 esxi6 esxi7 esxi8 coreos; do +for os in rhvh4 el7 el8 el9 el10 genesis suse15 ubuntu20.04 ubuntu18.04 ubuntu22.04 ubuntu24.04 esxi6 esxi7 esxi8 coreos; do mkdir -p %{buildroot}/opt/confluent/lib/osdeploy/$os/initramfs mkdir -p %{buildroot}/opt/confluent/lib/osdeploy/$os/profiles cp ${os}out/addons.* %{buildroot}/opt/confluent/lib/osdeploy/$os/initramfs diff --git a/confluent_server/confluent/osimage.py b/confluent_server/confluent/osimage.py index 8b9101ec..dcfdf6ec 100644 --- a/confluent_server/confluent/osimage.py +++ b/confluent_server/confluent/osimage.py @@ -315,6 +315,7 @@ def check_alma(isoinfo): ver = None arch = None cat = None + suffix = "" for entry in isoinfo[0]: if 'almalinux-release-8' in entry: ver = entry.split('-')[2] @@ -326,6 +327,12 @@ def check_alma(isoinfo): arch = entry.split('.')[-2] cat = 'el9' break + elif 'almalinux-kitten-release-10' in entry: + ver = entry.split('-')[3] + arch = entry.split('.')[-2] + cat = 'el10' + suffix = '_kitten' + break else: return None if arch == 'noarch' and '.discinfo' in isoinfo[1]: @@ -333,7 +340,7 @@ def check_alma(isoinfo): arch = prodinfo.split(b'\n')[2] if not isinstance(arch, str): arch = arch.decode('utf-8') - return {'name': 'alma-{0}-{1}'.format(ver, arch), 'method': EXTRACT, 'category': cat} + return {'name': 'alma{0}-{1}-{2}'.format(suffix, ver, arch), 'method': EXTRACT, 'category': cat} def check_centos(isoinfo): @@ -365,6 +372,12 @@ def check_centos(isoinfo): cat = 'el9' isstream = '_stream' break + elif 'centos-stream-release-10' in entry: + ver = entry.split('-')[3] + arch = entry.split('.')[-2] + cat = 'el10' + isstream = '_stream' + break elif 'centos-linux-release-8' in entry: ver = entry.split('-')[3] arch = entry.split('.')[-2] From b46a1e14a32b34dd13ed42a211e3107847c0a6a1 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 25 Oct 2024 12:11:58 -0400 Subject: [PATCH 27/53] Fix video console when first run has multiple nodes If client requested more than one on a fresh confluent run, then only one of the video consoles would properly wait. Fix by wrapping the assure in a startingup check. --- confluent_server/confluent/vinzmanager.py | 37 ++++++++++++++--------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/confluent_server/confluent/vinzmanager.py b/confluent_server/confluent/vinzmanager.py index 308acb59..016bad3c 100644 --- a/confluent_server/confluent/vinzmanager.py +++ b/confluent_server/confluent/vinzmanager.py @@ -16,27 +16,34 @@ mountsbyuser = {} _vinzfd = None _vinztoken = None webclient = eventlet.import_patched('pyghmi.util.webclient') - +startingup = False # 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) + global startingup + while startingup: + eventlet.sleep(0.5) + try: + startingup = True + 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) + _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) + finally: + startingup = False _unix_by_nodename = {} def get_url(nodename, inputdata): From 008c1308b49227a0864751be8e73eaeca21af63e Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Sat, 26 Oct 2024 08:16:56 -0400 Subject: [PATCH 28/53] Handle nvm subsystem without driver. A variant of the M.2 RAID enablement kit does not manifest with nvme driver. Address this by allowing 'nvm' subsystype. to allow blank driver. Also, to be on the safe side, have self.driver always be a string, so it can be 'falsey' but still work as a string. --- .../el7-diskless/profiles/default/scripts/getinstalldisk | 8 ++++++-- .../el7/profiles/default/scripts/getinstalldisk | 8 ++++++-- .../el8-diskless/profiles/default/scripts/getinstalldisk | 8 ++++++-- .../el8/profiles/default/scripts/getinstalldisk | 8 ++++++-- .../el9-diskless/profiles/default/scripts/getinstalldisk | 8 ++++++-- .../rhvh4/profiles/default/scripts/getinstalldisk | 8 ++++++-- .../suse15/profiles/hpc/scripts/getinstalldisk | 8 ++++++-- .../suse15/profiles/server/scripts/getinstalldisk | 8 ++++++-- .../profiles/default/scripts/getinstalldisk | 8 ++++++-- .../ubuntu20.04/profiles/default/scripts/getinstalldisk | 8 ++++++-- .../ubuntu22.04/profiles/default/scripts/getinstalldisk | 8 ++++++-- 11 files changed, 66 insertions(+), 22 deletions(-) diff --git a/confluent_osdeploy/el7-diskless/profiles/default/scripts/getinstalldisk b/confluent_osdeploy/el7-diskless/profiles/default/scripts/getinstalldisk index 04c7708e..c954a254 100644 --- a/confluent_osdeploy/el7-diskless/profiles/default/scripts/getinstalldisk +++ b/confluent_osdeploy/el7-diskless/profiles/default/scripts/getinstalldisk @@ -1,3 +1,4 @@ +#!/usr/bin/python3 import subprocess import os @@ -10,8 +11,9 @@ class DiskInfo(object): self.path = None self.model = '' self.size = 0 - self.driver = None + self.driver = '' self.mdcontainer = '' + self.subsystype = '' devnode = '/dev/{0}'.format(devname) qprop = subprocess.check_output( ['udevadm', 'info', '--query=property', devnode]) @@ -46,7 +48,9 @@ class DiskInfo(object): elif (k == 'DRIVERS' and not self.driver and v not in ('"sd"', '""')): self.driver = v.replace('"', '') - if not self.driver and 'imsm' not in self.mdcontainer: + elif k == 'ATTRS{subsystype}': + self.subsystype = v.replace('"', '') + if not self.driver and 'imsm' not in self.mdcontainer and self.subsystype != 'nvm': raise Exception("No driver detected") if os.path.exists('/sys/block/{0}/size'.format(self.name)): with open('/sys/block/{0}/size'.format(self.name), 'r') as sizesrc: diff --git a/confluent_osdeploy/el7/profiles/default/scripts/getinstalldisk b/confluent_osdeploy/el7/profiles/default/scripts/getinstalldisk index 04c7708e..c954a254 100644 --- a/confluent_osdeploy/el7/profiles/default/scripts/getinstalldisk +++ b/confluent_osdeploy/el7/profiles/default/scripts/getinstalldisk @@ -1,3 +1,4 @@ +#!/usr/bin/python3 import subprocess import os @@ -10,8 +11,9 @@ class DiskInfo(object): self.path = None self.model = '' self.size = 0 - self.driver = None + self.driver = '' self.mdcontainer = '' + self.subsystype = '' devnode = '/dev/{0}'.format(devname) qprop = subprocess.check_output( ['udevadm', 'info', '--query=property', devnode]) @@ -46,7 +48,9 @@ class DiskInfo(object): elif (k == 'DRIVERS' and not self.driver and v not in ('"sd"', '""')): self.driver = v.replace('"', '') - if not self.driver and 'imsm' not in self.mdcontainer: + elif k == 'ATTRS{subsystype}': + self.subsystype = v.replace('"', '') + if not self.driver and 'imsm' not in self.mdcontainer and self.subsystype != 'nvm': raise Exception("No driver detected") if os.path.exists('/sys/block/{0}/size'.format(self.name)): with open('/sys/block/{0}/size'.format(self.name), 'r') as sizesrc: diff --git a/confluent_osdeploy/el8-diskless/profiles/default/scripts/getinstalldisk b/confluent_osdeploy/el8-diskless/profiles/default/scripts/getinstalldisk index 04c7708e..c954a254 100644 --- a/confluent_osdeploy/el8-diskless/profiles/default/scripts/getinstalldisk +++ b/confluent_osdeploy/el8-diskless/profiles/default/scripts/getinstalldisk @@ -1,3 +1,4 @@ +#!/usr/bin/python3 import subprocess import os @@ -10,8 +11,9 @@ class DiskInfo(object): self.path = None self.model = '' self.size = 0 - self.driver = None + self.driver = '' self.mdcontainer = '' + self.subsystype = '' devnode = '/dev/{0}'.format(devname) qprop = subprocess.check_output( ['udevadm', 'info', '--query=property', devnode]) @@ -46,7 +48,9 @@ class DiskInfo(object): elif (k == 'DRIVERS' and not self.driver and v not in ('"sd"', '""')): self.driver = v.replace('"', '') - if not self.driver and 'imsm' not in self.mdcontainer: + elif k == 'ATTRS{subsystype}': + self.subsystype = v.replace('"', '') + if not self.driver and 'imsm' not in self.mdcontainer and self.subsystype != 'nvm': raise Exception("No driver detected") if os.path.exists('/sys/block/{0}/size'.format(self.name)): with open('/sys/block/{0}/size'.format(self.name), 'r') as sizesrc: diff --git a/confluent_osdeploy/el8/profiles/default/scripts/getinstalldisk b/confluent_osdeploy/el8/profiles/default/scripts/getinstalldisk index 04c7708e..c954a254 100644 --- a/confluent_osdeploy/el8/profiles/default/scripts/getinstalldisk +++ b/confluent_osdeploy/el8/profiles/default/scripts/getinstalldisk @@ -1,3 +1,4 @@ +#!/usr/bin/python3 import subprocess import os @@ -10,8 +11,9 @@ class DiskInfo(object): self.path = None self.model = '' self.size = 0 - self.driver = None + self.driver = '' self.mdcontainer = '' + self.subsystype = '' devnode = '/dev/{0}'.format(devname) qprop = subprocess.check_output( ['udevadm', 'info', '--query=property', devnode]) @@ -46,7 +48,9 @@ class DiskInfo(object): elif (k == 'DRIVERS' and not self.driver and v not in ('"sd"', '""')): self.driver = v.replace('"', '') - if not self.driver and 'imsm' not in self.mdcontainer: + elif k == 'ATTRS{subsystype}': + self.subsystype = v.replace('"', '') + if not self.driver and 'imsm' not in self.mdcontainer and self.subsystype != 'nvm': raise Exception("No driver detected") if os.path.exists('/sys/block/{0}/size'.format(self.name)): with open('/sys/block/{0}/size'.format(self.name), 'r') as sizesrc: diff --git a/confluent_osdeploy/el9-diskless/profiles/default/scripts/getinstalldisk b/confluent_osdeploy/el9-diskless/profiles/default/scripts/getinstalldisk index 04c7708e..c954a254 100644 --- a/confluent_osdeploy/el9-diskless/profiles/default/scripts/getinstalldisk +++ b/confluent_osdeploy/el9-diskless/profiles/default/scripts/getinstalldisk @@ -1,3 +1,4 @@ +#!/usr/bin/python3 import subprocess import os @@ -10,8 +11,9 @@ class DiskInfo(object): self.path = None self.model = '' self.size = 0 - self.driver = None + self.driver = '' self.mdcontainer = '' + self.subsystype = '' devnode = '/dev/{0}'.format(devname) qprop = subprocess.check_output( ['udevadm', 'info', '--query=property', devnode]) @@ -46,7 +48,9 @@ class DiskInfo(object): elif (k == 'DRIVERS' and not self.driver and v not in ('"sd"', '""')): self.driver = v.replace('"', '') - if not self.driver and 'imsm' not in self.mdcontainer: + elif k == 'ATTRS{subsystype}': + self.subsystype = v.replace('"', '') + if not self.driver and 'imsm' not in self.mdcontainer and self.subsystype != 'nvm': raise Exception("No driver detected") if os.path.exists('/sys/block/{0}/size'.format(self.name)): with open('/sys/block/{0}/size'.format(self.name), 'r') as sizesrc: diff --git a/confluent_osdeploy/rhvh4/profiles/default/scripts/getinstalldisk b/confluent_osdeploy/rhvh4/profiles/default/scripts/getinstalldisk index 04c7708e..c954a254 100644 --- a/confluent_osdeploy/rhvh4/profiles/default/scripts/getinstalldisk +++ b/confluent_osdeploy/rhvh4/profiles/default/scripts/getinstalldisk @@ -1,3 +1,4 @@ +#!/usr/bin/python3 import subprocess import os @@ -10,8 +11,9 @@ class DiskInfo(object): self.path = None self.model = '' self.size = 0 - self.driver = None + self.driver = '' self.mdcontainer = '' + self.subsystype = '' devnode = '/dev/{0}'.format(devname) qprop = subprocess.check_output( ['udevadm', 'info', '--query=property', devnode]) @@ -46,7 +48,9 @@ class DiskInfo(object): elif (k == 'DRIVERS' and not self.driver and v not in ('"sd"', '""')): self.driver = v.replace('"', '') - if not self.driver and 'imsm' not in self.mdcontainer: + elif k == 'ATTRS{subsystype}': + self.subsystype = v.replace('"', '') + if not self.driver and 'imsm' not in self.mdcontainer and self.subsystype != 'nvm': raise Exception("No driver detected") if os.path.exists('/sys/block/{0}/size'.format(self.name)): with open('/sys/block/{0}/size'.format(self.name), 'r') as sizesrc: diff --git a/confluent_osdeploy/suse15/profiles/hpc/scripts/getinstalldisk b/confluent_osdeploy/suse15/profiles/hpc/scripts/getinstalldisk index 04c7708e..c954a254 100644 --- a/confluent_osdeploy/suse15/profiles/hpc/scripts/getinstalldisk +++ b/confluent_osdeploy/suse15/profiles/hpc/scripts/getinstalldisk @@ -1,3 +1,4 @@ +#!/usr/bin/python3 import subprocess import os @@ -10,8 +11,9 @@ class DiskInfo(object): self.path = None self.model = '' self.size = 0 - self.driver = None + self.driver = '' self.mdcontainer = '' + self.subsystype = '' devnode = '/dev/{0}'.format(devname) qprop = subprocess.check_output( ['udevadm', 'info', '--query=property', devnode]) @@ -46,7 +48,9 @@ class DiskInfo(object): elif (k == 'DRIVERS' and not self.driver and v not in ('"sd"', '""')): self.driver = v.replace('"', '') - if not self.driver and 'imsm' not in self.mdcontainer: + elif k == 'ATTRS{subsystype}': + self.subsystype = v.replace('"', '') + if not self.driver and 'imsm' not in self.mdcontainer and self.subsystype != 'nvm': raise Exception("No driver detected") if os.path.exists('/sys/block/{0}/size'.format(self.name)): with open('/sys/block/{0}/size'.format(self.name), 'r') as sizesrc: diff --git a/confluent_osdeploy/suse15/profiles/server/scripts/getinstalldisk b/confluent_osdeploy/suse15/profiles/server/scripts/getinstalldisk index 04c7708e..c954a254 100644 --- a/confluent_osdeploy/suse15/profiles/server/scripts/getinstalldisk +++ b/confluent_osdeploy/suse15/profiles/server/scripts/getinstalldisk @@ -1,3 +1,4 @@ +#!/usr/bin/python3 import subprocess import os @@ -10,8 +11,9 @@ class DiskInfo(object): self.path = None self.model = '' self.size = 0 - self.driver = None + self.driver = '' self.mdcontainer = '' + self.subsystype = '' devnode = '/dev/{0}'.format(devname) qprop = subprocess.check_output( ['udevadm', 'info', '--query=property', devnode]) @@ -46,7 +48,9 @@ class DiskInfo(object): elif (k == 'DRIVERS' and not self.driver and v not in ('"sd"', '""')): self.driver = v.replace('"', '') - if not self.driver and 'imsm' not in self.mdcontainer: + elif k == 'ATTRS{subsystype}': + self.subsystype = v.replace('"', '') + if not self.driver and 'imsm' not in self.mdcontainer and self.subsystype != 'nvm': raise Exception("No driver detected") if os.path.exists('/sys/block/{0}/size'.format(self.name)): with open('/sys/block/{0}/size'.format(self.name), 'r') as sizesrc: diff --git a/confluent_osdeploy/ubuntu20.04-diskless/profiles/default/scripts/getinstalldisk b/confluent_osdeploy/ubuntu20.04-diskless/profiles/default/scripts/getinstalldisk index 04c7708e..c954a254 100644 --- a/confluent_osdeploy/ubuntu20.04-diskless/profiles/default/scripts/getinstalldisk +++ b/confluent_osdeploy/ubuntu20.04-diskless/profiles/default/scripts/getinstalldisk @@ -1,3 +1,4 @@ +#!/usr/bin/python3 import subprocess import os @@ -10,8 +11,9 @@ class DiskInfo(object): self.path = None self.model = '' self.size = 0 - self.driver = None + self.driver = '' self.mdcontainer = '' + self.subsystype = '' devnode = '/dev/{0}'.format(devname) qprop = subprocess.check_output( ['udevadm', 'info', '--query=property', devnode]) @@ -46,7 +48,9 @@ class DiskInfo(object): elif (k == 'DRIVERS' and not self.driver and v not in ('"sd"', '""')): self.driver = v.replace('"', '') - if not self.driver and 'imsm' not in self.mdcontainer: + elif k == 'ATTRS{subsystype}': + self.subsystype = v.replace('"', '') + if not self.driver and 'imsm' not in self.mdcontainer and self.subsystype != 'nvm': raise Exception("No driver detected") if os.path.exists('/sys/block/{0}/size'.format(self.name)): with open('/sys/block/{0}/size'.format(self.name), 'r') as sizesrc: diff --git a/confluent_osdeploy/ubuntu20.04/profiles/default/scripts/getinstalldisk b/confluent_osdeploy/ubuntu20.04/profiles/default/scripts/getinstalldisk index 04c7708e..c954a254 100644 --- a/confluent_osdeploy/ubuntu20.04/profiles/default/scripts/getinstalldisk +++ b/confluent_osdeploy/ubuntu20.04/profiles/default/scripts/getinstalldisk @@ -1,3 +1,4 @@ +#!/usr/bin/python3 import subprocess import os @@ -10,8 +11,9 @@ class DiskInfo(object): self.path = None self.model = '' self.size = 0 - self.driver = None + self.driver = '' self.mdcontainer = '' + self.subsystype = '' devnode = '/dev/{0}'.format(devname) qprop = subprocess.check_output( ['udevadm', 'info', '--query=property', devnode]) @@ -46,7 +48,9 @@ class DiskInfo(object): elif (k == 'DRIVERS' and not self.driver and v not in ('"sd"', '""')): self.driver = v.replace('"', '') - if not self.driver and 'imsm' not in self.mdcontainer: + elif k == 'ATTRS{subsystype}': + self.subsystype = v.replace('"', '') + if not self.driver and 'imsm' not in self.mdcontainer and self.subsystype != 'nvm': raise Exception("No driver detected") if os.path.exists('/sys/block/{0}/size'.format(self.name)): with open('/sys/block/{0}/size'.format(self.name), 'r') as sizesrc: diff --git a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/getinstalldisk b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/getinstalldisk index 04c7708e..c954a254 100644 --- a/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/getinstalldisk +++ b/confluent_osdeploy/ubuntu22.04/profiles/default/scripts/getinstalldisk @@ -1,3 +1,4 @@ +#!/usr/bin/python3 import subprocess import os @@ -10,8 +11,9 @@ class DiskInfo(object): self.path = None self.model = '' self.size = 0 - self.driver = None + self.driver = '' self.mdcontainer = '' + self.subsystype = '' devnode = '/dev/{0}'.format(devname) qprop = subprocess.check_output( ['udevadm', 'info', '--query=property', devnode]) @@ -46,7 +48,9 @@ class DiskInfo(object): elif (k == 'DRIVERS' and not self.driver and v not in ('"sd"', '""')): self.driver = v.replace('"', '') - if not self.driver and 'imsm' not in self.mdcontainer: + elif k == 'ATTRS{subsystype}': + self.subsystype = v.replace('"', '') + if not self.driver and 'imsm' not in self.mdcontainer and self.subsystype != 'nvm': raise Exception("No driver detected") if os.path.exists('/sys/block/{0}/size'.format(self.name)): with open('/sys/block/{0}/size'.format(self.name), 'r') as sizesrc: From a298ef8d7452fff5b96a4c7f80ebcda73206eaae Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 28 Oct 2024 09:41:38 -0400 Subject: [PATCH 29/53] Catch OpenBMC disconnects and handle them better --- confluent_server/confluent/plugins/console/openbmc.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/confluent_server/confluent/plugins/console/openbmc.py b/confluent_server/confluent/plugins/console/openbmc.py index 3ff08e5a..a67d9b23 100644 --- a/confluent_server/confluent/plugins/console/openbmc.py +++ b/confluent_server/confluent/plugins/console/openbmc.py @@ -124,7 +124,10 @@ class TsmConsole(conapi.Console): def recvdata(self): while self.connected: - pendingdata = self.ws.recv() + try: + pendingdata = self.ws.recv() + except websocket.WebSocketConnectionClosedException: + pendingdata = '' if pendingdata == '': self.datacallback(conapi.ConsoleEvent.Disconnect) return @@ -153,7 +156,10 @@ class TsmConsole(conapi.Console): return def write(self, data): - self.ws.send(data) + try: + self.ws.send(data) + except websocket.WebSocketConnectionClosedException: + self.datacallback(conapi.ConsoleEvent.Disconnect) def close(self): if self.recvr: From 9b6204db4f50f07408cbc7dc78b16be7859d997c Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 28 Oct 2024 13:21:54 -0400 Subject: [PATCH 30/53] Switch to the type of the member interface The 'team-slave/bond-slove' type is unneccesary, and messes up with infiniband. NetworkManager gets the idea if the 'ethernet' is a bond member without being told explicitly. --- confluent_osdeploy/common/profile/scripts/confignet | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/confluent_osdeploy/common/profile/scripts/confignet b/confluent_osdeploy/common/profile/scripts/confignet index 9092f631..6021c8d4 100644 --- a/confluent_osdeploy/common/profile/scripts/confignet +++ b/confluent_osdeploy/common/profile/scripts/confignet @@ -348,7 +348,8 @@ class NetworkManager(object): bondcfg[stg] = deats[stg] if member in self.uuidbyname: subprocess.check_call(['nmcli', 'c', 'del', self.uuidbyname[member]]) - subprocess.check_call(['nmcli', 'c', 'add', 'type', 'bond-slave', 'master', team, 'con-name', member, 'connection.interface-name', member]) + devtype = self.devtypes.get(member, 'bond-slave') + subprocess.check_call(['nmcli', 'c', 'add', 'type', devtype, 'master', team, 'con-name', member, 'connection.interface-name', member]) if bondcfg: args = [] for parm in bondcfg: From 523d5920bcb56dc1d36b3c434ad0f90657b203df Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 8 Nov 2024 12:10:15 -0500 Subject: [PATCH 31/53] Add a sample script for grabbing XCC screenshots --- misc/grabscreenshot.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 misc/grabscreenshot.py diff --git a/misc/grabscreenshot.py b/misc/grabscreenshot.py new file mode 100644 index 00000000..f1fe8a58 --- /dev/null +++ b/misc/grabscreenshot.py @@ -0,0 +1,31 @@ +import base64 +import pyghmi.redfish.command as ic +import pyghmi.util.webclient as webclient +import sys +import os +import time + +def iterm_draw(databuf): + datalen = len(databuf) + data = base64.b64encode(databuf).decode('utf8') + sys.stdout.write( + '\x1b]1337;File=inline=1;size={}:'.format(datalen)) + sys.stdout.write(data) + sys.stdout.write('\a') + sys.stdout.write('\n') + sys.stdout.flush() + + +i = ic.Command(sys.argv[1], os.environ['XCCUSER'], os.environ['XCCPASS'], verifycallback=lambda x: True) +i.get_health() +#url = '/download/Mini_ScreenShot.png?t={}'.format(int(time.time()*1000)) +i.oem.wc.grab_json_response('/api/providers/rp_screenshot') +url = '/download/HostScreenShot.png' +fd = webclient.FileDownloader(i.oem.wc, url, sys.argv[2]) +fd.start() +fd.join() +if sys.argv[3]: + imgdata = open(sys.argv[2], 'rb').read() + iterm_draw(imgdata) + + From d8c633a7d53496951dafd9feed0878e501838382 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 11 Nov 2024 08:03:57 -0500 Subject: [PATCH 32/53] Add localhost to ssh principals/equiv It shouldn't be possible to hijack localhost, so allow such addresses to be principaled and be listed in equiv. --- confluent_server/confluent/selfservice.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/confluent_server/confluent/selfservice.py b/confluent_server/confluent/selfservice.py index b7577b92..2486a71e 100644 --- a/confluent_server/confluent/selfservice.py +++ b/confluent_server/confluent/selfservice.py @@ -53,7 +53,7 @@ def listdump(input): def get_extra_names(nodename, cfg, myip=None): - names = set([]) + names = set(['127.0.0.1', '::1', 'localhost', 'localhost.localdomain']) dnsinfo = cfg.get_node_attributes(nodename, ('dns.*', 'net.*hostname')) dnsinfo = dnsinfo.get(nodename, {}) domain = dnsinfo.get('dns.domain', {}).get('value', None) @@ -631,4 +631,8 @@ def get_cluster_list(nodename=None, cfg=None): nodes.add(myname) if domain and domain not in myname: nodes.add('{0}.{1}'.format(myname, domain)) + nodes.add('::1') + nodes.add('127.0.0.1') + nodes.add('localhost') + nodes.add('localhost.domain') return nodes, domain From b1f8cf8f12b62e712f98020be701dbe9759d4e32 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 11 Nov 2024 13:48:21 -0500 Subject: [PATCH 33/53] Avoid redrawing 'powered off' redundantly. If the power state stays the same between queries, take no action to clear screen and draw redundant data. In the case of misreporting devices, it mitigates the impact of incorrect reporting, while generally preserving the output behavior when accurate. --- confluent_client/bin/confetty | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/confluent_client/bin/confetty b/confluent_client/bin/confetty index 0ce3cd5e..e1126df8 100755 --- a/confluent_client/bin/confetty +++ b/confluent_client/bin/confetty @@ -948,8 +948,9 @@ def main(): except IOError: pass if powerstate is None or powertime < time.time() - 10: # Check powerstate every 10 seconds + if powerstate == None: + powerstate = True powertime = time.time() - powerstate = True check_power_state() else: currcommand = prompt() From 5df30881f884c767b28738597314b7966d393a7c Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 11 Nov 2024 15:31:05 -0500 Subject: [PATCH 34/53] Provide resource to check if http has been initialized This allows clients to know if they need to direct user to do early setup procedures. --- confluent_server/confluent/httpapi.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/confluent_server/confluent/httpapi.py b/confluent_server/confluent/httpapi.py index 2cfbc117..675a62c3 100644 --- a/confluent_server/confluent/httpapi.py +++ b/confluent_server/confluent/httpapi.py @@ -655,6 +655,15 @@ def resourcehandler_backend(env, start_response): yield res return reqpath = env.get('PATH_INFO', '') + if reqpath == '/httpapi_initialized': + if (len(configmanager.ConfigManager(None).list_usergroups()) > 0 + or len(configmanager.ConfigManager(None).list_users()) > 0): + start_response('200 OK', headers) + yield '' + return + start_response('500 No authorized users', headers) + yield '' + return if reqpath.startswith('/boot/'): request = env['PATH_INFO'].split('/') if not request[0]: From 2f3a8619e898b485495692543d34f01c316c6172 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 12 Nov 2024 16:16:47 -0500 Subject: [PATCH 35/53] Fix vinz VNC for non-root users Relax permissions a tad to allow users to attempt to connect if they otherwise know the socket name. --- confluent_server/confluent/vinzmanager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/confluent_server/confluent/vinzmanager.py b/confluent_server/confluent/vinzmanager.py index 016bad3c..f9511676 100644 --- a/confluent_server/confluent/vinzmanager.py +++ b/confluent_server/confluent/vinzmanager.py @@ -31,7 +31,8 @@ def assure_vinz(): _vinztoken = base64.b64encode(os.urandom(33), altchars=b'_-').decode() os.environ['VINZ_TOKEN'] = _vinztoken os.makedirs('/var/run/confluent/vinz/sessions', exist_ok=True) - + os.chmod('/var/run/confluent/vinz', 0o711) + os.chmod('/var/run/confluent/vinz/sessions', 0o711) _vinzfd = subprocess.Popen( ['/opt/confluent/bin/vinz', '-c', '/var/run/confluent/vinz/control', From f88e9ecebf0e2a17679da33e9e1c973022b7353b Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 14 Nov 2024 08:03:34 -0500 Subject: [PATCH 36/53] Support discovery through second XCC NIC --- .../confluent/discovery/handlers/xcc.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/confluent_server/confluent/discovery/handlers/xcc.py b/confluent_server/confluent/discovery/handlers/xcc.py index ef009b6d..d5a5998f 100644 --- a/confluent_server/confluent/discovery/handlers/xcc.py +++ b/confluent_server/confluent/discovery/handlers/xcc.py @@ -627,6 +627,7 @@ class NodeHandler(immhandler.NodeHandler): '/redfish/v1/AccountService/Accounts/1', updateinf, method='PATCH') if targbmc and not targbmc.startswith('fe80::'): + attribsuffix = '' newip = targbmc.split('/', 1)[0] newipinfo = getaddrinfo(newip, 0)[0] newip = newipinfo[-1][0] @@ -636,6 +637,25 @@ class NodeHandler(immhandler.NodeHandler): newmask = netutil.cidr_to_mask(netconfig['prefix']) currinfo = wc.grab_json_response('/api/providers/logoninfo') currip = currinfo.get('items', [{}])[0].get('ipv4_address', '') + curreth1 = wc.grab_json_response('/api/dataset/imm_ethernet') + if curreth1: + if self.ipaddr.startswith('fe80::'): + ipkey = 'ipv6_link_local_address' + elif '.' in self.ipaddr: + ipkey = 'ipv4_address' + else: + raise Exception('Non-Link-Local IPv6 TODO') + nic1ip = curreth1.get('items', [{}])[0].get(ipkey, None) + if nic1ip != self.ipaddr: + # check second nic instead + curreth2 = wc.grab_json_response('/api/dataset/imm_ethernet_2') + if curreth2 and curreth2.get('items', [{}])[0].get('if_second_port_exist', 0): + nic2ip = curreth2.get('items', [{}])[0].get(ipkey + '_2', None) + if nic2ip != self.ipaddr: + raise Exception("Unable to determine which NIC is active") + # ok, second nic is active, target it + currip = curreth2.get('items', [{}])[0].get("ipv4_address", None) + attribsuffix = '_2' # do not change the ipv4_config if the current config looks right already if currip != newip: statargs = { @@ -646,6 +666,10 @@ class NodeHandler(immhandler.NodeHandler): statargs['ENET_IPv4GatewayIPAddr'] = netconfig['ipv4_gateway'] elif not netutil.address_is_local(newip): raise exc.InvalidArgumentException('Will not remotely configure a device with no gateway') + if attribsuffix: + for currkey in list(statargs): + statargs[currkey + attribsuffix] = statargs[currkey] + del statargs[currkey] netset, status = wc.grab_json_response_with_status('/api/dataset', statargs) print(repr(netset)) print(repr(status)) From 9c589e83527e10edceceaf558c8dd126d3d0bb93 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 15 Nov 2024 09:19:51 -0500 Subject: [PATCH 37/53] Regenerate initrd after install The drivers on target may differ from source, regenerate initramfs to allow for booting --- .../el9-diskless/profiles/default/scripts/post.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/confluent_osdeploy/el9-diskless/profiles/default/scripts/post.sh b/confluent_osdeploy/el9-diskless/profiles/default/scripts/post.sh index 7a7ac01e..cb548bf4 100644 --- a/confluent_osdeploy/el9-diskless/profiles/default/scripts/post.sh +++ b/confluent_osdeploy/el9-diskless/profiles/default/scripts/post.sh @@ -40,6 +40,9 @@ run_remote_parts post.d # Induce execution of remote configuration, e.g. ansible plays in ansible/post.d/ run_remote_config post.d +# rebuild initrd, pick up new drivers if needed +dracut -f /boot/initramfs-$(uname -r).img $(uname -r) + curl -sf -X POST -d 'status: staged' -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $confluent_apikey" https://$confluent_websrv/confluent-api/self/updatestatus kill $logshowpid From a0ffc11d6fb6c9d672f2ffa6bdf2ca7b1338d259 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 15 Nov 2024 09:24:04 -0500 Subject: [PATCH 38/53] Expand type for GUI usage --- confluent_server/confluent/config/attributes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/confluent_server/confluent/config/attributes.py b/confluent_server/confluent/config/attributes.py index f926c962..ff2aa90a 100644 --- a/confluent_server/confluent/config/attributes.py +++ b/confluent_server/confluent/config/attributes.py @@ -94,8 +94,8 @@ node = { 'considered a member'), }, 'type': { - 'description': ('Classification of node as server or switch. By default a node is presumed to be a server.'), - 'validvalues': ('switch', 'server'), + 'description': ('The type of node. This may be switch, server, rackmount, dense, enclosure or not set to be generic.'), + 'validvalues': ('switch', 'server', 'rackmount', 'dense', 'enclosure', ''), }, 'crypted.rootpassword': { 'description': 'The password of the local root password. ' From 8e89c8f6227e520d4eb38042d0ed76b8c0c57d3e Mon Sep 17 00:00:00 2001 From: Tinashe Date: Tue, 19 Nov 2024 09:39:29 -0500 Subject: [PATCH 39/53] use webauthn instead of webauthn-rp --- confluent_server/LICENSE | 177 +++++++++++ confluent_server/VERSION | 1 + confluent_server/confluent/webauthn.py | 311 ++++++-------------- confluent_server/confluent_server.spec.tmpl | 8 +- 4 files changed, 274 insertions(+), 223 deletions(-) create mode 100644 confluent_server/LICENSE create mode 100644 confluent_server/VERSION diff --git a/confluent_server/LICENSE b/confluent_server/LICENSE new file mode 100644 index 00000000..f433b1a5 --- /dev/null +++ b/confluent_server/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/confluent_server/VERSION b/confluent_server/VERSION new file mode 100644 index 00000000..d05e1762 --- /dev/null +++ b/confluent_server/VERSION @@ -0,0 +1 @@ +3.11.2~dev56+ga0ffc11d diff --git a/confluent_server/confluent/webauthn.py b/confluent_server/confluent/webauthn.py index 4920ac33..99e652c0 100644 --- a/confluent_server/confluent/webauthn.py +++ b/confluent_server/confluent/webauthn.py @@ -1,26 +1,22 @@ -from webauthn_rp.registrars import CredentialData import confluent.tlvdata as tlvdata import confluent.util as util import json import copy import base64 - - import secrets, time from typing import Any, Optional -from webauthn_rp.backends import CredentialsBackend -from webauthn_rp.builders import * -from webauthn_rp.converters import cose_key, jsonify -from webauthn_rp.errors import WebAuthnRPError -from webauthn_rp.parsers import parse_cose_key, parse_public_key_credential -from webauthn_rp.registrars import * -from webauthn_rp.types import ( - AttestationObject, AttestationType, AuthenticatorAssertionResponse, - AuthenticatorAttestationResponse, AuthenticatorData, - COSEAlgorithmIdentifier, PublicKeyCredential, - PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, - PublicKeyCredentialRpEntity, PublicKeyCredentialType, - PublicKeyCredentialUserEntity, TrustedPath) +from webauthn import ( + generate_registration_options, + options_to_json, + generate_authentication_options, + ) +from webauthn.helpers.structs import ( + AuthenticatorSelectionCriteria, + UserVerificationRequirement, +) + +from webauthn import verify_registration_response +from webauthn import verify_authentication_response challenges = {} @@ -34,15 +30,12 @@ class Credential(): self.credential_public_key = public_key class Challenge(): - def __init__(self, request, timstamp_ms, id=None) -> None: + def __init__(self, request, id=None) -> None: if id is None: self.id = util.randomstring(16) else: self.id = id self.request = request - self.timestamp_ms = timstamp_ms - - def _load_credentials(creds): if creds is None: @@ -55,7 +48,8 @@ def _load_credentials(creds): def _load_authenticators(authenticators): ret = authenticators if 'challenges' in ret: - ret['challenges']['request'] = base64.b64decode(ret['challenges']['request']) + if not ret['challenges'] is None: + ret['challenges']['request'] = base64.b64decode(ret['challenges']['request']) if 'credentials' in ret: ret['credentials'] = _load_credentials(ret['credentials']) return ret @@ -78,7 +72,7 @@ class User(): def __parse_challenges(self): if self.challenges: request = base64.b64encode(self.challenges.request).decode() - return {"id": self.challenges.id, 'request': request, 'timestamp_ms': self.challenges.timestamp_ms} + return {"id": self.challenges.id, 'request': request} @staticmethod @@ -97,8 +91,7 @@ class User(): #for now leaving signature count as None return (Credential(id=credential["id"], signature_count=None, public_key=credential["credential_public_key"]), username) return None - - + @staticmethod def get_credential(credential_id, username): @@ -112,13 +105,11 @@ class User(): if credential_id is None: return Credential(id=credential["id"], signature_count=credential["signature_count"], public_key=credential["credential_public_key"]) - if credential["id"] == credential_id: - return Credential(id=credential["id"], signature_count=credential["signature_count"], public_key=credential["credential_public_key"]) return None @staticmethod - def get_challenge(challengeID, username): + def get_challenge(username): if not isinstance(username, str): username = username.decode('utf8') authuser = CONFIG_MANAGER.get_user(username) @@ -127,10 +118,8 @@ class User(): authenticators = authuser.get('authenticators', {}) authenticators = _load_authenticators(authenticators) challenge = authenticators['challenges'] - if challenge["id"] == challengeID: - return Challenge(request=challenge["request"], timstamp_ms=challenge["timestamp_ms"], id=challenge["id"]) - - return None + return Challenge(request=challenge["request"], id=challenge["id"]) + @staticmethod def get(username): @@ -155,7 +144,7 @@ class User(): authid = base64.b64decode(b64authid) challenge = authenticators.get("challenges", None) if challenge: - challenges_return = Challenge(challenge['request'], challenge['timestamp_ms'], id=challenge["id"]) + challenges_return = Challenge(challenge['request'], id=challenge["id"]) credential = authenticators.get("credentials", None) if credential: @@ -187,221 +176,107 @@ class User(): #raise Exception("Credential item not found") -def timestamp_ms(): - return int(time.time() * 1000) - - -class RegistrarImpl(CredentialsRegistrar): - def register_credential_attestation( - self, - credential: PublicKeyCredential, - att: AttestationObject, - att_type: AttestationType, - user: PublicKeyCredentialUserEntity, - rp: PublicKeyCredentialRpEntity, - trusted_path: Optional[TrustedPath] = None) -> Any: - - assert att.auth_data is not None - assert att.auth_data.attested_credential_data is not None - cpk = att.auth_data.attested_credential_data.credential_public_key - - user_model = User.get(user.name) - if user_model is None: - return 'No user found' - - credential_model = Credential(id=credential.raw_id, signature_count=None, public_key=cose_key(cpk)) - user_model.add(credential_model) - user_model.save() - - def register_credential_assertion( - self, - credential: PublicKeyCredential, - authenticator_data: AuthenticatorData, - user: PublicKeyCredentialUserEntity, - rp: PublicKeyCredentialRpEntity) -> Any: - - user_model = User.get(user.name) - credential_model = User.get_credential(credential_id=credential.raw_id, username=user.name) - credential_model.signature_count = None - user_model.update(credential_model) - user_model.save() - - def get_credential_data( - self, - credential_id: bytes) -> Optional[CredentialData]: - - #credential_model = User.get_credential(credential_id=credential_id, username=username) - (credential_model, username) = User.seek_credential_by_id(credential_id) - user_model = User.get(username) - - return CredentialData( - parse_cose_key(credential_model.credential_public_key), - credential_model.signature_count, - PublicKeyCredentialUserEntity( - name=user_model.username, - id=user_model.user_handle, - display_name=user_model.username - ) - ) - - - -APP_TIMEOUT = 60000 - - - - - - -APP_CREDENTIALS_BACKEND = CredentialsBackend(RegistrarImpl()) - def registration_request(username, cfg, APP_RELYING_PARTY): - - APP_CCO_BUILDER = CredentialCreationOptionsBuilder( - rp=APP_RELYING_PARTY, - pub_key_cred_params=[ - PublicKeyCredentialParameters(type=PublicKeyCredentialType.PUBLIC_KEY, - alg=COSEAlgorithmIdentifier.Value.ES256) - ], - timeout=APP_TIMEOUT, - ) - user_model = User.get(username) if user_model is None: raise Exception("User not foud") - - challenge_bytes = secrets.token_bytes(64) - challenge = Challenge(request=challenge_bytes, timstamp_ms=timestamp_ms()) - user_model.add(challenge) - user_model.save() - options = APP_CCO_BUILDER.build( - user=PublicKeyCredentialUserEntity( - name=username, - id=user_model.user_handle, - display_name=username + options = generate_registration_options( + rp_name=APP_RELYING_PARTY.name, + rp_id=APP_RELYING_PARTY.id, + user_id=user_model.user_handle, + user_name=username, + authenticator_selection=AuthenticatorSelectionCriteria( + user_verification=UserVerificationRequirement.REQUIRED, ), - challenge=challenge_bytes ) - options_json = jsonify(options) - return { - 'challengeID': challenge.id, - 'creationOptions': options_json - } + challenge = Challenge(options.challenge) + user_model.add(challenge) + user_model.save() + options_json = options_to_json(options) + return options_json + def registration_response(request, username, APP_RELYING_PARTY, APP_ORIGIN): - try: - challengeID = request["challengeID"] - credential = parse_public_key_credential(json.loads(request["credential"])) - except Exception: - raise Exception("Could not parse input data") - - if type(credential.response) is not AuthenticatorAttestationResponse: - raise Exception("Invalid response type") - - challenge_model = User.get_challenge(challengeID, username) + challenge_model = User.get_challenge(username) if not challenge_model: raise Exception("Could not find challenge matching given id") user_model = User.get(username) if not user_model: raise Exception("Invalid Username") - - current_timestamp = timestamp_ms() - if current_timestamp - challenge_model.timestamp_ms > APP_TIMEOUT: - return "Timeout" - - - user_entity = PublicKeyCredentialUserEntity(name=user_model.username, id=user_model.user_handle, display_name=user_model.username) + try: - APP_CREDENTIALS_BACKEND.handle_credential_attestation( - credential=credential, - user=user_entity, - rp=APP_RELYING_PARTY, - expected_challenge=challenge_model.request, - expected_origin=APP_ORIGIN + registration_verification = verify_registration_response( + credential=request, + expected_challenge=challenge_model.request, + expected_rp_id=APP_RELYING_PARTY.id, + expected_origin=APP_ORIGIN, + require_user_verification=True, ) - except WebAuthnRPError as wrp: + except Exception as err: raise Exception("Could not handle credential attestation") - return True + credential = Credential(id=registration_verification.credential_id, signature_count=registration_verification.sign_count, public_key=registration_verification.credential_public_key) + user_model.add(credential) + user_model.save() + + return {"verified": True} def authentication_request(username, APP_RELYING_PARTY): - APP_CRO_BUILDER = CredentialRequestOptionsBuilder( - rp_id=APP_RELYING_PARTY.id, - timeout=APP_TIMEOUT, - ) - - user_model = User.get(username) - - if user_model is None: - return 'User not registered' - - credential = user_model.get_credential(None, username) - if credential is None: - return 'No credential found' - - challenge_bytes = secrets.token_bytes(64) - challenge = Challenge(request=challenge_bytes, timstamp_ms=timestamp_ms()) - user_model.add(challenge) - user_model.save() - - options = APP_CRO_BUILDER.build( - challenge=challenge_bytes, - allow_credentials=[ - PublicKeyCredentialDescriptor( - id=credential.id, - type=PublicKeyCredentialType.PUBLIC_KEY - ) - ] - ) - - options_json = jsonify(options) - return { - 'challengeID': challenge.id, - 'requestOptions': options_json - } - -def authentication_response(request, username, APP_RELYING_PARTY, APP_ORIGIN): - try: - challengeID = request["challengeID"] - credential = parse_public_key_credential(json.loads(request["credential"])) - except Exception: - raise Exception("Could not parse input data") - - if type(credential.response) is not AuthenticatorAssertionResponse: - raise Exception('Invalid response type') - - challenge_model = User.get_challenge(challengeID, username) - if not challenge_model: - raise Exception("Could not find challenge matching given id") - user_model = User.get(username) if not user_model: raise Exception("Invalid Username") + + options = generate_authentication_options( + rp_id=APP_RELYING_PARTY.id, + user_verification=UserVerificationRequirement.REQUIRED, + ) + + challenge = Challenge(options.challenge) + user_model.add(challenge) + user_model.save() - current_timestamp = timestamp_ms() - if current_timestamp - challenge_model.timestamp_ms > APP_TIMEOUT: - return "Timeout" - - user_entity = PublicKeyCredentialUserEntity(name=user_model.username, id=user_model.user_handle, display_name=user_model.username) + opts = options_to_json(options) + return opts + +def authentication_response(request, username, APP_RELYING_PARTY, APP_ORIGIN): + user_model = User.get(username) + if not user_model: + raise Exception("Invalid Username") + + challenge_model = User.get_challenge(username) + if not challenge_model: + raise Exception("Could not find challenge matching given id") + + credential_model = User.get_credential(credential_id=None, username=username) + if not credential_model: + raise Exception("No credential for user") try: - APP_CREDENTIALS_BACKEND.handle_credential_assertion( - credential=credential, - user=user_entity, - rp=APP_RELYING_PARTY, - expected_challenge=challenge_model.request, - expected_origin=APP_ORIGIN - ) - except WebAuthnRPError: - raise Exception('Could not handle credential assertion') + print(request) + except Exception: + raise Exception("Could not parse input data") + verification = verify_authentication_response( + credential=request, + expected_challenge=challenge_model.request, + expected_rp_id=APP_RELYING_PARTY.id, + expected_origin=APP_ORIGIN, + credential_public_key = credential_model.credential_public_key, + credential_current_sign_count = 0, + require_user_verification = True + + ) + print(verification) return {"verified": True} - +class RpEntity(object): + def __init__(self, name, id): + self.name = name + self.id = id def handle_api_request(url, env, start_response, username, cfm, headers, reqbody, authorized): """ @@ -413,7 +288,7 @@ def handle_api_request(url, env, start_response, username, cfm, headers, reqbody APP_ORIGIN = 'https://' + env['HTTP_X_FORWARDED_HOST'] HOST = env['HTTP_X_FORWARDED_HOST'] - APP_RELYING_PARTY = PublicKeyCredentialRpEntity(name='Confluent Web UI', id=HOST) + APP_RELYING_PARTY = RpEntity(name='Confluent Web UI', id=HOST) if env['REQUEST_METHOD'] != 'POST': raise Exception('Only POST supported for webauthn operations') @@ -465,9 +340,7 @@ def handle_api_request(url, env, start_response, username, cfm, headers, reqbody if not isinstance(username, bytes): username = username.encode('utf8') rsp = registration_response(req, username, APP_RELYING_PARTY, APP_ORIGIN) - if rsp == 'Timeout': - start_response('408 Timeout', headers) - else: + if rsp.get('verified', False): print('worked out') start_response('200 OK', headers) yield json.dumps({'status': 'Success'}) diff --git a/confluent_server/confluent_server.spec.tmpl b/confluent_server/confluent_server.spec.tmpl index 2d3af243..8110b83a 100644 --- a/confluent_server/confluent_server.spec.tmpl +++ b/confluent_server/confluent_server.spec.tmpl @@ -19,15 +19,15 @@ BuildArch: noarch Requires: confluent_vtbufferd %if "%{dist}" == ".el7" -Requires: python-pyghmi >= 1.5.71, python-eventlet, python-greenlet, python-pycryptodomex >= 3.4.7, confluent_client == %{version}, python-pyparsing, python-paramiko, python-webauthn-rp, 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.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 %else %if "%{dist}" == ".el8" -Requires: python3-pyghmi >= 1.5.71, python3-eventlet, python3-greenlet, python3-pycryptodomex >= 3.4.7, confluent_client == %{version}, python3-pyparsing, python3-paramiko, python3-webauthn-rp, 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.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 %else %if "%{dist}" == ".el9" -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-webauthn-rp, 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.5.71, python3-eventlet, python3-greenlet, python3-pycryptodomex >= 3.4.7, confluent_client == %{version}, python3-pyparsing, python3-paramiko, python3-dns, python3-webauthn, 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 %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-webauthn-rp, 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-webauthn, 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 From efda4b4ef7c33748a358651277315ad3a3b97f70 Mon Sep 17 00:00:00 2001 From: Tinashe Date: Tue, 19 Nov 2024 10:08:56 -0500 Subject: [PATCH 40/53] remove LICENCE,VERSION and devel prints --- confluent_server/confluent/webauthn.py | 8 +------- confluent_server/confluent_server.spec.tmpl | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/confluent_server/confluent/webauthn.py b/confluent_server/confluent/webauthn.py index 99e652c0..509a7584 100644 --- a/confluent_server/confluent/webauthn.py +++ b/confluent_server/confluent/webauthn.py @@ -254,11 +254,6 @@ def authentication_response(request, username, APP_RELYING_PARTY, APP_ORIGIN): credential_model = User.get_credential(credential_id=None, username=username) if not credential_model: raise Exception("No credential for user") - - try: - print(request) - except Exception: - raise Exception("Could not parse input data") verification = verify_authentication_response( credential=request, @@ -270,7 +265,7 @@ def authentication_response(request, username, APP_RELYING_PARTY, APP_ORIGIN): require_user_verification = True ) - print(verification) + return {"verified": True} class RpEntity(object): @@ -341,7 +336,6 @@ def handle_api_request(url, env, start_response, username, cfm, headers, reqbody username = username.encode('utf8') rsp = registration_response(req, username, APP_RELYING_PARTY, APP_ORIGIN) if rsp.get('verified', False): - print('worked out') start_response('200 OK', headers) yield json.dumps({'status': 'Success'}) diff --git a/confluent_server/confluent_server.spec.tmpl b/confluent_server/confluent_server.spec.tmpl index 8110b83a..1fb62d71 100644 --- a/confluent_server/confluent_server.spec.tmpl +++ b/confluent_server/confluent_server.spec.tmpl @@ -27,7 +27,7 @@ Requires: python3-pyghmi >= 1.5.71, python3-eventlet, python3-greenlet, python3- %if "%{dist}" == ".el9" 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-webauthn, 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 %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-webauthn, 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 From ff523a0d5c6c1573741eecd735d4458deb76356b Mon Sep 17 00:00:00 2001 From: Tinashe Date: Tue, 19 Nov 2024 10:10:02 -0500 Subject: [PATCH 41/53] change the server spec file --- confluent_server/LICENSE | 177 --------------------------------------- confluent_server/VERSION | 1 - 2 files changed, 178 deletions(-) delete mode 100644 confluent_server/LICENSE delete mode 100644 confluent_server/VERSION diff --git a/confluent_server/LICENSE b/confluent_server/LICENSE deleted file mode 100644 index f433b1a5..00000000 --- a/confluent_server/LICENSE +++ /dev/null @@ -1,177 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS diff --git a/confluent_server/VERSION b/confluent_server/VERSION deleted file mode 100644 index d05e1762..00000000 --- a/confluent_server/VERSION +++ /dev/null @@ -1 +0,0 @@ -3.11.2~dev56+ga0ffc11d From f1f433041cd66c4f4cbe856ed83b507d1beb0c5b Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 26 Nov 2024 12:09:04 -0500 Subject: [PATCH 42/53] Restore underscore headers Eventlet has "helpfully" stopped supporting headers with underscores. Restore them since we want to support backwards compatibility and do not have the option to just ignore existing clients. --- confluent_server/confluent/httpapi.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/confluent_server/confluent/httpapi.py b/confluent_server/confluent/httpapi.py index 675a62c3..f3c0b1af 100644 --- a/confluent_server/confluent/httpapi.py +++ b/confluent_server/confluent/httpapi.py @@ -623,6 +623,9 @@ def wsock_handler(ws): def resourcehandler(env, start_response): + for hdr in env['headers_raw']: + if hdr[0].startswith('CONFLUENT_'): + env['HTTP_' + hdr[0]] = hdr[1] try: if 'HTTP_SEC_WEBSOCKET_VERSION' in env: for rsp in wsock_handler(env, start_response): From 3fa6b47995bfbb74190d5d6e709270317902841a Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 2 Dec 2024 16:50:13 -0500 Subject: [PATCH 43/53] Do not json dumps a string that is already json --- confluent_server/confluent/webauthn.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/confluent_server/confluent/webauthn.py b/confluent_server/confluent/webauthn.py index 509a7584..4ca39057 100644 --- a/confluent_server/confluent/webauthn.py +++ b/confluent_server/confluent/webauthn.py @@ -238,7 +238,6 @@ def authentication_request(username, APP_RELYING_PARTY): challenge = Challenge(options.challenge) user_model.add(challenge) user_model.save() - opts = options_to_json(options) return opts @@ -300,7 +299,7 @@ def handle_api_request(url, env, start_response, username, cfm, headers, reqbody cfm.set_user(username, {'webauthid': b64authid}) opts = registration_request(username, cfm, APP_RELYING_PARTY) start_response('200 OK', headers) - yield json.dumps(opts) + yield opts elif url.startswith('/registered_credentials/'): username = url.rsplit('/', 1)[-1] userinfo = cfm.get_user(username) @@ -308,7 +307,7 @@ def handle_api_request(url, env, start_response, username, cfm, headers, reqbody username = username.encode('utf8') opts = authentication_request(username, APP_RELYING_PARTY) start_response('200 OK', headers) - yield json.dumps(opts) + yield opts elif url.startswith('/validate/'): username = url.rsplit('/', 1)[-1] userinfo = cfm.get_user(username) From 8bdabdc962dda7406d18feae8ea00717280ec924 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 3 Dec 2024 14:34:11 -0500 Subject: [PATCH 44/53] SMMv3 discovery support The SMMv3 doesn't respond to the correct SSDP service, add the odd service. Have SMMv3 use the standard redfish handler. Augment the standard redfish handler to deal with non-error password change required message. --- confluent_server/confluent/discovery/core.py | 4 ++++ .../confluent/discovery/handlers/redfishbmc.py | 9 ++++++++- .../confluent/discovery/protocols/ssdp.py | 12 +++++++++--- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/confluent_server/confluent/discovery/core.py b/confluent_server/confluent/discovery/core.py index fd302f8b..41767a77 100644 --- a/confluent_server/confluent/discovery/core.py +++ b/confluent_server/confluent/discovery/core.py @@ -75,6 +75,7 @@ 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.smm3 as smm3 import confluent.discovery.handlers.megarac as megarac import confluent.exceptions as exc import confluent.log as log @@ -114,6 +115,7 @@ class nesteddict(dict): nodehandlers = { 'service:lenovo-smm': smm, 'service:lenovo-smm2': smm, + 'lenovo-smm3': smm3, 'lenovo-xcc': xcc, 'lenovo-xcc3': xcc3, 'megarac-bmc': megarac, @@ -134,6 +136,7 @@ servicenames = { 'cumulus-switch': 'cumulus-switch', 'service:lenovo-smm': 'lenovo-smm', 'service:lenovo-smm2': 'lenovo-smm2', + 'lenovo-smm3': 'lenovo-smm3', 'affluent-switch': 'affluent-switch', 'lenovo-xcc': 'lenovo-xcc', 'lenovo-xcc3': 'lenovo-xcc3', @@ -151,6 +154,7 @@ servicebyname = { 'cumulus-switch': 'cumulus-switch', 'lenovo-smm': 'service:lenovo-smm', 'lenovo-smm2': 'service:lenovo-smm2', + 'lenovo-smm3': 'lenovo-smm3', 'affluent-switch': 'affluent-switch', 'lenovo-xcc': 'lenovo-xcc', 'lenovo-xcc3': 'lenovo-xcc3', diff --git a/confluent_server/confluent/discovery/handlers/redfishbmc.py b/confluent_server/confluent/discovery/handlers/redfishbmc.py index 7cf3f3d1..a14530d3 100644 --- a/confluent_server/confluent/discovery/handlers/redfishbmc.py +++ b/confluent_server/confluent/discovery/handlers/redfishbmc.py @@ -134,7 +134,14 @@ class NodeHandler(generic.NodeHandler): rsp = json.loads(rsp) currerr = rsp.get('error', {}) ecode = currerr.get('code', None) - if ecode.endswith('PasswordChangeRequired'): + if not ecode: + for msg in rsp['@Message.ExtendedInfo']: + if 'PasswordChangeRequired' in msg['MessageId']: + chgurl = msg['MessageArgs'][0] + break + else: + raise Exception("Failed to ascertain login failure reason") + elif ecode.endswith('PasswordChangeRequired'): for einfo in currerr.get('@Message.ExtendedInfo', []): if einfo.get('MessageId', None).endswith('PasswordChangeRequired'): for msgarg in einfo.get('MessageArgs'): diff --git a/confluent_server/confluent/discovery/protocols/ssdp.py b/confluent_server/confluent/discovery/protocols/ssdp.py index 5c27473b..8a9ab396 100644 --- a/confluent_server/confluent/discovery/protocols/ssdp.py +++ b/confluent_server/confluent/discovery/protocols/ssdp.py @@ -58,7 +58,7 @@ smsg = ('M-SEARCH * HTTP/1.1\r\n' def active_scan(handler, protocol=None): known_peers = set([]) - for scanned in scan(['urn:dmtf-org:service:redfish-rest:1', 'urn::service:affluent']): + for scanned in scan(['urn:dmtf-org:service:redfish-rest:1', 'urn::dmtf-org:service:redfish-rest:', 'urn::service:affluent']): for addr in scanned['addresses']: addr = addr[0:1] + addr[2:] if addr in known_peers: @@ -429,10 +429,10 @@ def _find_service(service, target): mya['enclosure-machinetype-model'] = [val] yield peerdata[nid] continue - 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')) + elif '/redfish/v1/' not in peerdata[nid].get('urls', ()) and '/redfish/v1' not in peerdata[nid].get('urls', ()): + continue else: for targurl in peerdata[nid]['urls']: if '/eth' in targurl and targurl.endswith('.xml'): @@ -463,6 +463,12 @@ def check_fish(urldata, port=443, verifycallback=None): return None if url == '/DeviceDescription.json': if not peerinfo: + if data['services'] == ['urn::dmtf-org:service:redfish-rest:']: + peerinfo = wc.grab_json_response('/redfish/v1/') + if peerinfo: + data['services'] = ['lenovo-smm3'] + data['uuid'] = peerinfo['UUID'].lower() + return data return None try: peerinfo = peerinfo[0] From 64895c9f95039d0426ee8c62cf22dc36b586c93e Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 3 Dec 2024 14:55:50 -0500 Subject: [PATCH 45/53] Return empty hifurl list when non existent For systems without a host interface, properly show an empty list. --- confluent_server/confluent/discovery/handlers/redfishbmc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_server/confluent/discovery/handlers/redfishbmc.py b/confluent_server/confluent/discovery/handlers/redfishbmc.py index a14530d3..5ecd078d 100644 --- a/confluent_server/confluent/discovery/handlers/redfishbmc.py +++ b/confluent_server/confluent/discovery/handlers/redfishbmc.py @@ -32,7 +32,7 @@ def get_host_interface_urls(wc, mginfo): returls = [] hifurl = mginfo.get('HostInterfaces', {}).get('@odata.id', None) if not hifurl: - return None + return [] hifinfo = wc.grab_json_response(hifurl) hifurls = hifinfo.get('Members', []) for hifurl in hifurls: From 5f90aa4f6934064d3f62f11be747f34ee72e9bb9 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 3 Dec 2024 15:00:10 -0500 Subject: [PATCH 46/53] Add SMMv3 handler for SMMv3 discovery --- .../confluent/discovery/handlers/smm3.py | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 confluent_server/confluent/discovery/handlers/smm3.py diff --git a/confluent_server/confluent/discovery/handlers/smm3.py b/confluent_server/confluent/discovery/handlers/smm3.py new file mode 100644 index 00000000..bea1525e --- /dev/null +++ b/confluent_server/confluent/discovery/handlers/smm3.py @@ -0,0 +1,56 @@ +# 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): + devname = 'SMM3' + + def get_firmware_default_account_info(self): + return ('USERID', 'PASSW0RD') + + +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 23cb1fa8abd5bc83ca129b5cdb6d44ba1093130e Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 5 Dec 2024 10:18:16 -0500 Subject: [PATCH 47/53] Refresh genesis license handling --- genesis/exlicenses/bash/NOTICE | 54 ++++ genesis/exlicenses/dhcp-common/NOTICE | 157 ++++++++++++ .../exlicenses/libgcrypt/LICENSES.ppc-aes-gcm | 238 ++++++++++++++++++ genesis/exlicenses/libsepol/NOTICE | 54 ++++ genesis/getlicenses.py | 9 +- 5 files changed, 508 insertions(+), 4 deletions(-) create mode 100644 genesis/exlicenses/bash/NOTICE create mode 100644 genesis/exlicenses/dhcp-common/NOTICE create mode 100644 genesis/exlicenses/libgcrypt/LICENSES.ppc-aes-gcm create mode 100644 genesis/exlicenses/libsepol/NOTICE diff --git a/genesis/exlicenses/bash/NOTICE b/genesis/exlicenses/bash/NOTICE new file mode 100644 index 00000000..5d581e30 --- /dev/null +++ b/genesis/exlicenses/bash/NOTICE @@ -0,0 +1,54 @@ +Copyright and licensing for additional files included in the bash package: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Files: lib/sh/inet_aton.c +Copyright: 1983, 1990, 1993 The Regents of the University of California. All rights reserved. +License: BSD-4-UC AND HPND + +* Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * This product includes software developed by the University of + * California, Berkeley and its contributors. + * 4. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * - + * Portions Copyright (c) 1993 by Digital Equipment Corporation. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies, and that + * the name of Digital Equipment Corporation not be used in advertising or + * publicity pertaining to distribution of the document or software without + * specific, written prior permission. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND DIGITAL EQUIPMENT CORP. DISCLAIMS ALL + * WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL DIGITAL EQUIPMENT + * CORPORATION BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL + * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR + * PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS + * ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS + * SOFTWARE. + * - + * --Copyright-- \ No newline at end of file diff --git a/genesis/exlicenses/dhcp-common/NOTICE b/genesis/exlicenses/dhcp-common/NOTICE new file mode 100644 index 00000000..08aa9830 --- /dev/null +++ b/genesis/exlicenses/dhcp-common/NOTICE @@ -0,0 +1,157 @@ +Copyright and licensing for additional files included in the dhcp package: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Files: + server/ldap_krb_helper.c + omapip/inet_addr.c +Copyright: Copyright (c) 2015 by Internet Systems Consortium, Inc. ("ISC") All rights reserved. +License: BSD-3 + + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of The Internet Software Consortium nor the names + * of its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE INTERNET SOFTWARE CONSORTIUM AND + * CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE INTERNET SOFTWARE CONSORTIUM OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * This helper was written by William Brown , + * inspired by krb5_helper.c from bind-dyndb-ldap by Simo Sorce (Redhat) + +Files: server/ldap_casa.c +Copyright: Copyright (c) 2006 Novell, Inc. +License: BSD-3 AND ISC + + * All rights reserved. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1.Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2.Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3.Neither the name of ISC, ISC DHCP, nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + + * THIS SOFTWARE IS PROVIDED BY INTERNET SYSTEMS CONSORTIUM AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ISC OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + + * This file was written by S Kalyanasundaram + */ + +/* + * Copyright (c) 2004-2010 by Internet Systems Consortium, Inc. ("ISC") + * Copyright (c) 1995-2003 by Internet Software Consortium + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT + * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * Internet Systems Consortium, Inc. + * 950 Charter Street + * Redwood City, CA 94063 + * + * https://www.isc.org/ + +Files: server/ldap.c.cloexec + server/ldap.c +Copyright: + Copyright (c) 2010,2015-2016 by Internet Systems Consortium, Inc. ("ISC") + Copyright (c) 2003-2006 Ntelos, Inc. + All rights reserved. +License: BSD-3 + + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of The Internet Software Consortium nor the names + * of its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE INTERNET SOFTWARE CONSORTIUM AND + * CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE INTERNET SOFTWARE CONSORTIUM OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * This LDAP module was written by Brian Masney . Its + * development was sponsored by Ntelos, Inc. (www.ntelos.com). + +Files: omapip/inet_addr.c +Copyright: Copyright (c) 1983, 1990, 1993 The Regents of the University of California. All rights reserved. +License: BSD-3-UC + + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. \ No newline at end of file diff --git a/genesis/exlicenses/libgcrypt/LICENSES.ppc-aes-gcm b/genesis/exlicenses/libgcrypt/LICENSES.ppc-aes-gcm new file mode 100644 index 00000000..f6733a69 --- /dev/null +++ b/genesis/exlicenses/libgcrypt/LICENSES.ppc-aes-gcm @@ -0,0 +1,238 @@ +Additional license notices for Libgcrypt. -*- org -*- + +This file contains the copying permission notices for various files in +the Libgcrypt distribution which are not covered by the GNU Lesser +General Public License (LGPL) or the GNU General Public License (GPL). + +These notices all require that a copy of the notice be included +in the accompanying documentation and be distributed with binary +distributions of the code, so be sure to include this file along +with any binary distributions derived from the GNU C Library. + +* BSD_3Clause + + For files: + - cipher/sha256-avx-amd64.S + - cipher/sha256-avx2-bmi2-amd64.S + - cipher/sha256-ssse3-amd64.S + - cipher/sha512-avx-amd64.S + - cipher/sha512-avx2-bmi2-amd64.S + - cipher/sha512-ssse3-amd64.S + +#+begin_quote + Copyright (c) 2012, Intel Corporation + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the + distribution. + + * Neither the name of the Intel Corporation nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + + THIS SOFTWARE IS PROVIDED BY INTEL CORPORATION "AS IS" AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL INTEL CORPORATION OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#+end_quote + + + For files: + - random/jitterentropy-base.c + - random/jitterentropy.h + - random/rndjent.c (plus common Libgcrypt copyright holders) + +#+begin_quote + * Copyright Stephan Mueller , 2013 + * + * License + * ======= + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, and the entire permission notice in its entirety, + * including the disclaimer of warranties. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * ALTERNATIVELY, this product may be distributed under the terms of + * the GNU General Public License, in which case the provisions of the GPL are + * required INSTEAD OF the above restrictions. (This clause is + * necessary due to a potential bad interaction between the GPL and + * the restrictions contained in a BSD-style copyright.) + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ALL OF + * WHICH ARE HEREBY DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF NOT ADVISED OF THE POSSIBILITY OF SUCH + * DAMAGE. +#+end_quote + +* X License + + For files: + - install.sh + +#+begin_quote + Copyright (C) 1994 X Consortium + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + X CONSORTIUM BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNEC- + TION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + Except as contained in this notice, the name of the X Consortium shall not + be used in advertising or otherwise to promote the sale, use or other deal- + ings in this Software without prior written authorization from the X Consor- + tium. +#+end_quote + +* Public domain + + For files: + - cipher/arcfour-amd64.S + +#+begin_quote + Author: Marc Bevand + Licence: I hereby disclaim the copyright on this code and place it + in the public domain. +#+end_quote + +* OCB license 1 + + For files: + - cipher/cipher-ocb.c + +#+begin_quote + OCB is covered by several patents but may be used freely by most + software. See http://web.cs.ucdavis.edu/~rogaway/ocb/license.htm . + In particular license 1 is suitable for Libgcrypt: See + http://web.cs.ucdavis.edu/~rogaway/ocb/license1.pdf for the full + license document; it basically says: + + License 1 — License for Open-Source Software Implementations of OCB + (Jan 9, 2013) + + Under this license, you are authorized to make, use, and + distribute open-source software implementations of OCB. This + license terminates for you if you sue someone over their + open-source software implementation of OCB claiming that you have + a patent covering their implementation. + + + + License for Open Source Software Implementations of OCB + January 9, 2013 + + 1 Definitions + + 1.1 “Licensor” means Phillip Rogaway. + + 1.2 “Licensed Patents” means any patent that claims priority to United + States Patent Application No. 09/918,615 entitled “Method and Apparatus + for Facilitating Efficient Authenticated Encryption,” and any utility, + divisional, provisional, continuation, continuations-in-part, reexamination, + reissue, or foreign counterpart patents that may issue with respect to the + aforesaid patent application. This includes, but is not limited to, United + States Patent No. 7,046,802; United States Patent No. 7,200,227; United + States Patent No. 7,949,129; United States Patent No. 8,321,675 ; and any + patent that issues out of United States Patent Application No. 13/669,114. + + 1.3 “Use” means any practice of any invention claimed in the Licensed Patents. + + 1.4 “Software Implementation” means any practice of any invention + claimed in the Licensed Patents that takes the form of software executing on + a user-programmable, general-purpose computer or that takes the form of a + computer-readable medium storing such software. Software Implementation does + not include, for example, application-specific integrated circuits (ASICs), + field-programmable gate arrays (FPGAs), embedded systems, or IP cores. + + 1.5 “Open Source Software” means software whose source code is published + and made available for inspection and use by anyone because either (a) the + source code is subject to a license that permits recipients to copy, modify, + and distribute the source code without payment of fees or royalties, or + (b) the source code is in the public domain, including code released for + public use through a CC0 waiver. All licenses certified by the Open Source + Initiative at opensource.org as of January 9, 2013 and all Creative Commons + licenses identified on the creativecommons.org website as of January 9, + 2013, including the Public License Fallback of the CC0 waiver, satisfy these + requirements for the purposes of this license. + + 1.6 “Open Source Software Implementation” means a Software + Implementation in which the software implicating the Licensed Patents is + Open Source Software. Open Source Software Implementation does not include + any Software Implementation in which the software implicating the Licensed + Patents is combined, so as to form a larger program, with software that is + not Open Source Software. + + 2 License Grant + + 2.1 License. Subject to your compliance with the term s of this license, + including the restriction set forth in Section 2.2, Licensor hereby + grants to you a perpetual, worldwide, non-exclusive, non-transferable, + non-sublicenseable, no-charge, royalty-free, irrevocable license to practice + any invention claimed in the Licensed Patents in any Open Source Software + Implementation. + + 2.2 Restriction. If you or your affiliates institute patent litigation + (including, but not limited to, a cross-claim or counterclaim in a lawsuit) + against any entity alleging that any Use authorized by this license + infringes another patent, then any rights granted to you under this license + automatically terminate as of the date such litigation is filed. + + 3 Disclaimer + YOUR USE OF THE LICENSED PATENTS IS AT YOUR OWN RISK AND UNLESS REQUIRED + BY APPLICABLE LAW, LICENSOR MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY + KIND CONCERNING THE LICENSED PATENTS OR ANY PRODUCT EMBODYING ANY LICENSED + PATENT, EXPRESS OR IMPLIED, STATUT ORY OR OTHERWISE, INCLUDING, WITHOUT + LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR + PURPOSE, OR NONINFRINGEMENT. IN NO EVENT WILL LICENSOR BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, + ARISING FROM OR RELATED TO ANY USE OF THE LICENSED PATENTS, INCLUDING, + WITHOUT LIMITATION, DIRECT, INDIRECT, INCIDENTAL, CONSEQUENTIAL, PUNITIVE + OR SPECIAL DAMAGES, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF + SUCH DAMAGES PRIOR TO SUCH AN OCCURRENCE. +#+end_quote diff --git a/genesis/exlicenses/libsepol/NOTICE b/genesis/exlicenses/libsepol/NOTICE new file mode 100644 index 00000000..4bb15d21 --- /dev/null +++ b/genesis/exlicenses/libsepol/NOTICE @@ -0,0 +1,54 @@ +Copyright and licensing for additional files included in the libsepol package: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Files: cil/* +Copyright: Copyright 2011, 2014 Tresys Technology, LLC. All rights reserved. +License: BSD-2 + + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY TRESYS TECHNOLOGY, LLC ``AS IS'' AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL TRESYS TECHNOLOGY, LLC OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those + * of the authors and should not be interpreted as representing official policies, + * either expressed or implied, of Tresys Technology, LLC. + +Files: cil/test/unit/CuTest.c +Copyright: Copyright (c) 2003 Asim Jalis +License: Zlib + + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software in + * a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * + * 2. Altered source versions must be plainly marked as such, and must not + * be misrepresented as being the original software. + * + * 3. This notice may not be removed or altered from any source + * distribution. \ No newline at end of file diff --git a/genesis/getlicenses.py b/genesis/getlicenses.py index e87e2786..380fc517 100644 --- a/genesis/getlicenses.py +++ b/genesis/getlicenses.py @@ -76,7 +76,7 @@ manuallicenses = [ '/usr/share/licenses/lz4/LICENSE.BSD', '/usr/share/licenses/nss/LICENSE.APACHE', # http://www.apache.org/licenses/LICENSE-2.0 '/usr/share/licenses/openssh/COPYING.blowfish', # from header of blowfish file in bsd-compat - '/usr/share/licenses/bc/COPYING.GPLv2', + '/usr/share/licenses/bc/COPYING.GPLv2', # generic copy of GPLv2 '/usr/share/licenses/bind-license/LICENSE', # MPLv2 from the source code '/usr/share/licenses/procps-ng/COPYING.LIBv2.1', # fetched internet # cp /usr/share/doc/lz4-libs/LICENSE /usr/share/licenses/lz4/LICENSE.BSD @@ -87,7 +87,7 @@ manuallicenses = [ '/usr/share/licenses/pcre/LICENSE.BSD2', # stack-less just in time compiler, Zoltan Herzeg '/usr/share/licenses/sqlite/LICENSE.md', # https://raw.githubusercontent.com/sqlite/sqlite/master/LICENSE.md '/usr/share/licenses/pcre2/LICENSE.BSD2', - '/usr/share/licenses/dhcp-common/NOTICE', + '/usr/share/licenses/dhcp-common/NOTICE', # from exlicenses '/usr/share/licenses/xz/COPYING.GPLv3', # manually extracted from xz source '/usr/share/licenses/bash/NOTICE', '/usr/share/licenses/libsepol/NOTICE', @@ -111,6 +111,7 @@ manuallicenses = [ '/usr/share/licenses/tmux/NOTICE', # built by extracttmuxlicenses.py '/usr/share/licenses/tmux/COPYING', # extracted from source '/usr/share/licenses/tmux/README', # extracted from source + # yum download kernel soruce, cp -a from LICENSES to kernel-extra '/usr/share/licenses/kernel-extra/preferred/BSD-2-Clause', '/usr/share/licenses/kernel-extra/preferred/BSD-3-Clause', '/usr/share/licenses/kernel-extra/preferred/BSD-3-Clause-Clear', @@ -132,10 +133,10 @@ manuallicenses = [ '/usr/share/licenses/kernel-extra/exceptions/GCC-exception-2.0', '/usr/share/licenses/kernel-extra/exceptions/Linux-syscall-note', '/usr/share/licenses/util-linux/COPYING.GPLv3', # extract from parse-date.c, from srpm - '/usr/share/licenses/kmod/COPYING', # GPL not LGPL, must extract from kmod srpm + '/usr/share/licenses/kmod/COPYING', # GPL not LGPL, must extract from kmod srpm, tools subdir '/usr/share/licenses/krb5-libs/NOTICE', # copy it verbatim from LICENSE, exact same file '/usr/share/doc/less/README', - '/usr/share/centos-release/EULA', + '/usr/share/almalinux-release/EULA', #'/usr/share/doc/almalinux-release/GPL', '/usr/share/licenses/libcap-ng-utils/COPYING', '/usr/share/licenses/libdb/copyright', # from libdb, db-5.3.28, lang/sql/odbc/debian/copyright From 5d6a935beca711f2c7b98f1186bcb59ff6fa3b70 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 5 Dec 2024 10:21:12 -0500 Subject: [PATCH 48/53] Bump genesis version --- genesis/confluent-genesis.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/genesis/confluent-genesis.spec b/genesis/confluent-genesis.spec index beaeb5cd..e652ed5e 100644 --- a/genesis/confluent-genesis.spec +++ b/genesis/confluent-genesis.spec @@ -1,5 +1,5 @@ %define arch x86_64 -Version: 3.10.0 +Version: 3.12.0 Release: 1 Name: confluent-genesis-%{arch} BuildArch: noarch From 2c9b526de4901f4e3ee8d804f5b479aaa76afe3c Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 5 Dec 2024 14:26:26 -0500 Subject: [PATCH 49/53] Repeat the interface loop for Ubuntu identity deploy It may happen that the first pass at nics misses a viable network interface due to slow link up or slow spanning tree forwarding. Repeat the loop through the interfaces to have follow up chances at success. --- .../initramfs/scripts/init-premount/confluent | 31 ++++++++++--------- .../initramfs/scripts/init-premount/confluent | 31 ++++++++++--------- 2 files changed, 34 insertions(+), 28 deletions(-) diff --git a/confluent_osdeploy/ubuntu20.04/initramfs/scripts/init-premount/confluent b/confluent_osdeploy/ubuntu20.04/initramfs/scripts/init-premount/confluent index ef09db40..528b27d6 100755 --- a/confluent_osdeploy/ubuntu20.04/initramfs/scripts/init-premount/confluent +++ b/confluent_osdeploy/ubuntu20.04/initramfs/scripts/init-premount/confluent @@ -31,23 +31,26 @@ if [ -e /dev/disk/by-label/CNFLNT_IDNT ]; then MYGW="" fi MYNM=$(grep ^ipv4_netmask: $tcfg | awk '{print $2}') - for NICGUESS in $(ip link|grep LOWER_UP|grep -v LOOPBACK|cut -d ' ' -f 2 | sed -e 's/:$//'); do - ip addr add dev $NICGUESS $v4addr - if [ ! -z "$MYGW" ]; then - ip route add default via $MYGW - fi - for dsrv in $deploysrvs; do - if openssl s_client -connect $dsrv:443 > /dev/null 2>&1; then - deploysrvs=$dsrv - NIC=$NICGUESS + NIC="" + while [ -z "$NIC" ]; do + for NICGUESS in $(ip link|grep LOWER_UP|grep -v LOOPBACK|cut -d ' ' -f 2 | sed -e 's/:$//'); do + ip addr add dev $NICGUESS $v4addr + if [ ! -z "$MYGW" ]; then + ip route add default via $MYGW + fi + for dsrv in $deploysrvs; do + if openssl s_client -connect $dsrv:443 > /dev/null 2>&1; then + deploysrvs=$dsrv + NIC=$NICGUESS + break + fi + done + if [ -z "$NIC" ]; then + ip -4 a flush dev $NICGUESS + else break fi done - if [ -z "$NIC" ]; then - ip -4 a flush dev $NICGUESS - else - break - fi done ipconfig -d $MYIP::$MYGW:$MYNM::$NIC echo $NIC > /tmp/autodetectnic diff --git a/confluent_osdeploy/ubuntu22.04/initramfs/scripts/init-premount/confluent b/confluent_osdeploy/ubuntu22.04/initramfs/scripts/init-premount/confluent index 8a7e3777..6315ba5d 100755 --- a/confluent_osdeploy/ubuntu22.04/initramfs/scripts/init-premount/confluent +++ b/confluent_osdeploy/ubuntu22.04/initramfs/scripts/init-premount/confluent @@ -31,23 +31,26 @@ if [ -e /dev/disk/by-label/CNFLNT_IDNT ]; then MYGW="" fi MYNM=$(grep ^ipv4_netmask: $tcfg | awk '{print $2}') - for NICGUESS in $(ip link|grep LOWER_UP|grep -v LOOPBACK|cut -d ' ' -f 2 | sed -e 's/:$//'); do - ip addr add dev $NICGUESS $v4addr - if [ ! -z "$MYGW" ]; then - ip route add default via $MYGW - fi - for dsrv in $deploysrvs; do - if openssl s_client -connect $dsrv:443 > /dev/null 2>&1; then - deploysrvs=$dsrv - NIC=$NICGUESS + NIC="" + while [ -z "$NIC" ]; do + for NICGUESS in $(ip link|grep LOWER_UP|grep -v LOOPBACK|cut -d ' ' -f 2 | sed -e 's/:$//'); do + ip addr add dev $NICGUESS $v4addr + if [ ! -z "$MYGW" ]; then + ip route add default via $MYGW + fi + for dsrv in $deploysrvs; do + if openssl s_client -connect $dsrv:443 > /dev/null 2>&1; then + deploysrvs=$dsrv + NIC=$NICGUESS + break + fi + done + if [ -z "$NIC" ]; then + ip -4 a flush dev $NICGUESS + else break fi done - if [ -z "$NIC" ]; then - ip -4 a flush dev $NICGUESS - else - break - fi done ipconfig -d $MYIP::$MYGW:$MYNM::$NIC echo $NIC > /tmp/autodetectnic From ddd97388a60305cd0b2af752533f146ea838424d Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 11 Dec 2024 10:22:10 -0500 Subject: [PATCH 50/53] Implement discovery for newer SMMv3 firmware --- .../confluent/discovery/handlers/smm3.py | 13 +++++++++++++ .../confluent/discovery/protocols/ssdp.py | 8 +++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/confluent_server/confluent/discovery/handlers/smm3.py b/confluent_server/confluent/discovery/handlers/smm3.py index bea1525e..7e663dc2 100644 --- a/confluent_server/confluent/discovery/handlers/smm3.py +++ b/confluent_server/confluent/discovery/handlers/smm3.py @@ -26,6 +26,19 @@ getaddrinfo = eventlet.support.greendns.getaddrinfo class NodeHandler(redfishbmc.NodeHandler): devname = 'SMM3' + def scan(self): + attrs = self.info.get('attributes', {}) + 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() + modelname = attrs.get('enclosure-component-name', None) + if modelname: + modelname = modelname.split(' MT:')[0] + self.info['modelname'] = modelname + def get_firmware_default_account_info(self): return ('USERID', 'PASSW0RD') diff --git a/confluent_server/confluent/discovery/protocols/ssdp.py b/confluent_server/confluent/discovery/protocols/ssdp.py index 8a9ab396..28f14fe8 100644 --- a/confluent_server/confluent/discovery/protocols/ssdp.py +++ b/confluent_server/confluent/discovery/protocols/ssdp.py @@ -463,7 +463,7 @@ def check_fish(urldata, port=443, verifycallback=None): return None if url == '/DeviceDescription.json': if not peerinfo: - if data['services'] == ['urn::dmtf-org:service:redfish-rest:']: + if data.get('services', None) == ['urn::dmtf-org:service:redfish-rest:']: peerinfo = wc.grab_json_response('/redfish/v1/') if peerinfo: data['services'] = ['lenovo-smm3'] @@ -485,6 +485,12 @@ def check_fish(urldata, port=443, verifycallback=None): data['services'] = ['lenovo-xcc'] if 'xcc-variant' not in peerinfo else ['lenovo-xcc' + peerinfo['xcc-variant']] return data except (IndexError, KeyError): + if 'type' in peerinfo and peerinfo['type'].lower() == 'lenovo-smm3': + del peerinfo['xcc-variant'] + data['uuid'] = peerinfo['enclosure-uuid'] + data['services'] = ['lenovo-smm3'] + data['attributes'] = peerinfo + return data return None url = '/redfish/v1/' peerinfo = wc.grab_json_response('/redfish/v1/') From 8e0bc4300839cfb3a10e51911ebdf5f58f04aaec Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 13 Dec 2024 16:15:23 -0500 Subject: [PATCH 51/53] Fix for SMMv3 onboarding --- confluent_server/confluent/discovery/handlers/redfishbmc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_server/confluent/discovery/handlers/redfishbmc.py b/confluent_server/confluent/discovery/handlers/redfishbmc.py index 5ecd078d..5f3c34fb 100644 --- a/confluent_server/confluent/discovery/handlers/redfishbmc.py +++ b/confluent_server/confluent/discovery/handlers/redfishbmc.py @@ -109,7 +109,7 @@ class NodeHandler(generic.NodeHandler): self.target_account_url(wc)) acctinfo = acctinfo[0] actypes = acctinfo['AccountTypes'] - candidates = acctinfo['AccountTypes@Redfish.AllowableValues'] + candidates = acctinfo.get('AccountTypes@Redfish.AllowableValues', []) if 'IPMI' not in actypes and 'IPMI' in candidates: actypes.append('IPMI') acctupd = { From cd2509c4853502482bf1eea2269ba9c38385fd7f Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 13 Dec 2024 16:16:12 -0500 Subject: [PATCH 52/53] Ignore unparseable net config files If some pre-processing has rendered config files unparseable, ignore the file as we can't intelligently rewrite those. --- confluent_osdeploy/common/profile/scripts/confignet | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/confluent_osdeploy/common/profile/scripts/confignet b/confluent_osdeploy/common/profile/scripts/confignet index 6021c8d4..71c156a7 100644 --- a/confluent_osdeploy/common/profile/scripts/confignet +++ b/confluent_osdeploy/common/profile/scripts/confignet @@ -226,7 +226,11 @@ class WickedManager(object): self.cfgbydev[devname] = currcfg for cfg in open(ifcfg).read().splitlines(): cfg = cfg.split('#', 1)[0] - kv = ' '.join(shlex.split(cfg)).split('=', 1) + try: + kv = ' '.join(shlex.split(cfg)).split('=', 1) + except Exception: + # unparseable line, likely having something we can't handle + del self.cfgbydev[devname] if len(kv) != 2: continue k, v = kv From 0cae0fe06ec774b5a4950624f3d67ff6547f0484 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 13 Dec 2024 19:04:15 -0500 Subject: [PATCH 53/53] Add installtodisk support for el9 diskless images --- .../common/profile/scripts/setupssh | 9 +++ .../profiles/default/scripts/image2disk.py | 62 ++++++++++++++++--- .../profiles/default/scripts/installimage | 2 + .../profiles/default/scripts/post.sh | 6 +- 4 files changed, 71 insertions(+), 8 deletions(-) diff --git a/confluent_osdeploy/common/profile/scripts/setupssh b/confluent_osdeploy/common/profile/scripts/setupssh index eb989bb7..06ae3e01 100644 --- a/confluent_osdeploy/common/profile/scripts/setupssh +++ b/confluent_osdeploy/common/profile/scripts/setupssh @@ -10,6 +10,15 @@ for pubkey in /etc/ssh/ssh_host*key.pub; do rm $certfile confluentpython $confapiclient /confluent-api/self/sshcert $pubkey -o $certfile done +if [ -d /etc/ssh/sshd_config.d/ -a ! -e /etc/ssh/sshd_config.d/90-confluent.conf ]; then + for cert in /etc/ssh/ssh*-cert.pub; do + echo HostCertificate $cert >> /etc/ssh/sshd_config.d/90-confluent.conf + done + echo HostbasedAuthentication yes >> /etc/ssh/sshd_config.d/90-confluent.conf + echo HostbasedUsesNameFromPacketOnly yes >> /etc/ssh/sshd_config.d/90-confluent.conf + echo IgnoreRhosts no >> /etc/ssh/sshd_config.d/90-confluent.conf +fi + TMPDIR=$(mktemp -d) cd $TMPDIR confluentpython $confapiclient /confluent-public/site/initramfs.tgz -o initramfs.tgz diff --git a/confluent_osdeploy/el9-diskless/profiles/default/scripts/image2disk.py b/confluent_osdeploy/el9-diskless/profiles/default/scripts/image2disk.py index 6a924964..ccf36036 100644 --- a/confluent_osdeploy/el9-diskless/profiles/default/scripts/image2disk.py +++ b/confluent_osdeploy/el9-diskless/profiles/default/scripts/image2disk.py @@ -65,7 +65,11 @@ def get_image_metadata(imgpath): continue yield md else: - raise Exception('Installation from single part image not supported') + # plausible filesystem structure to apply to a nominally "diskless" image + yield {'mount': '/', 'filesystem': 'xfs', 'minsize': 39513563136, 'initsize': 954128662528, 'flags': 'rw,seclabel,relatime,attr2,inode64,logbufs=8,logbsize=32k,noquota', 'device': '/dev/mapper/root', 'compressed_size': 27022069760} + yield {'mount': '/boot', 'filesystem': 'xfs', 'minsize': 232316928, 'initsize': 1006632960, 'flags': 'rw,seclabel,relatime,attr2,inode64,logbufs=8,logbsize=32k,noquota', 'device': '/dev/nvme1n1p2', 'compressed_size': 171462656} + yield {'mount': '/boot/efi', 'filesystem': 'vfat', 'minsize': 7835648, 'initsize': 627900416, 'flags': 'rw,relatime,fmask=0077,dmask=0077,codepage=437,iocharset=ascii,shortname=winnt,errors=remount-ro', 'device': '/dev/nvme1n1p1', 'compressed_size': 1576960} + #raise Exception('Installation from single part image not supported') class PartedRunner(): def __init__(self, disk): @@ -84,8 +88,17 @@ def fixup(rootdir, vols): for vol in vols: devbymount[vol['mount']] = vol['targetdisk'] fstabfile = os.path.join(rootdir, 'etc/fstab') - with open(fstabfile) as tfile: - fstab = tfile.read().split('\n') + if os.path.exists(fstabfile): + with open(fstabfile) as tfile: + fstab = tfile.read().split('\n') + else: + #diskless image, need to invent fstab + fstab = [ + "#ORIGFSTAB#/dev/mapper/root# / xfs defaults 0 0", + "#ORIGFSTAB#UUID=aaf9e0f9-aa4d-4d74-9e75-3537620cfe23# /boot xfs defaults 0 0", + "#ORIGFSTAB#UUID=C21D-B881# /boot/efi vfat umask=0077,shortname=winnt 0 2", + "#ORIGFSTAB#/dev/mapper/swap# none swap defaults 0 0", + ] while not fstab[0]: fstab = fstab[1:] if os.path.exists(os.path.join(rootdir, '.autorelabel')): @@ -135,8 +148,10 @@ def fixup(rootdir, vols): newcfg = ifcfg.split('/')[-1] newcfg = os.path.join(rootdir, 'etc/NetworkManager/system-connections/{0}'.format(newcfg)) shutil.copy2(ifcfg, newcfg) - shutil.rmtree(os.path.join(rootdir, 'etc/confluent/')) - shutil.copytree('/etc/confluent', os.path.join(rootdir, 'etc/confluent')) + rootconfluentdir = os.path.join(rootdir, 'etc/confluent/') + if os.path.exists(rootconfluentdir): + shutil.rmtree(rootconfluentdir) + shutil.copytree('/etc/confluent', rootconfluentdir) if policy: sys.stdout.write('Applying SELinux labeling...') sys.stdout.flush() @@ -191,8 +206,24 @@ def fixup(rootdir, vols): else: newcfgparts.append(cfgpart) loadentout.write(' '.join(newcfgparts) + '\n') - with open(grubsyscfg) as defgrubin: - defgrub = defgrubin.read().split('\n') + if os.path.exists(grubsyscfg): + with open(grubsyscfg) as defgrubin: + defgrub = defgrubin.read().split('\n') + else: + defgrub = [ + 'GRUB_TIMEOUT=5', + 'GRUB_DISTRIBUTOR="$(sed ' + "'s, release .*$,,g'" + ' /etc/system-release)"', + 'GRUB_DEFAULT=saved', + 'GRUB_DISABLE_SUBMENU=true', + 'GRUB_TERMINAL=""', + 'GRUB_SERIAL_COMMAND=""', + 'GRUB_CMDLINE_LINUX="crashkernel=1G-4G:192M,4G-64G:256M,64G-:512M rd.lvm.lv=vg/root rd.lvm.lv=vg/swap"', + 'GRUB_DISABLE_RECOVERY="true"', + 'GRUB_ENABLE_BLSCFG=true', + ] + if not os.path.exists(os.path.join(rootdir, "etc/kernel/cmdline")): + with open(os.path.join(rootdir, "etc/kernel/cmdline"), "w") as cmdlineout: + cmdlineout.write("root=/dev/mapper/localstorage-root rd.lvm.lv=localstorage/root") with open(grubsyscfg, 'w') as defgrubout: for gline in defgrub: gline = gline.split() @@ -217,6 +248,12 @@ def fixup(rootdir, vols): grubcfg = grubcfg[:-1] if len(grubcfg) == 1: grubcfg = grubcfg[0] + elif not grubcfg: + grubcfg = '/boot/grub2/grub.cfg' + paths = glob.glob(os.path.join(rootdir, 'boot/efi/EFI/*')) + for path in paths: + with open(os.path.join(path, 'grub.cfg'), 'w') as stubgrubout: + stubgrubout.write("search --no-floppy --root-dev-only --fs-uuid --set=dev " + bootuuid + "\nset prefix=($dev)/grub2\nexport $prefix\nconfigfile $prefix/grub.cfg\n") else: for gcfg in grubcfg: rgcfg = os.path.join(rootdir, gcfg[1:]) # gcfg has a leading / to get rid of @@ -272,10 +309,19 @@ def fixup(rootdir, vols): shimpath = subprocess.check_output(['find', os.path.join(rootdir, 'boot/efi'), '-name', 'shimx64.efi']).decode('utf8').strip() shimpath = shimpath.replace(rootdir, '/').replace('/boot/efi', '').replace('//', '/').replace('/', '\\') subprocess.check_call(['efibootmgr', '-c', '-d', targblock, '-l', shimpath, '--part', partnum]) + + try: + os.makedirs(os.path.join(rootdir, 'opt/confluent/bin')) + except Exception: + pass + shutil.copy2('/opt/confluent/bin/apiclient', os.path.join(rootdir, 'opt/confluent/bin/apiclient')) #other network interfaces def had_swap(): + if not os.path.exists('/etc/fstab'): + # diskless source, assume swap + return True with open('/etc/fstab') as tabfile: tabs = tabfile.read().split('\n') for tab in tabs: @@ -440,6 +486,8 @@ def install_to_disk(imgpath): subprocess.check_call(['mount', vol['targetdisk'], '/run/imginst/targ']) source = vol['mount'].replace('/', '_') source = '/run/imginst/sources/' + source + if not os.path.exists(source): + source = '/run/imginst/sources/_' + vol['mount'] blankfsstat = os.statvfs('/run/imginst/targ') blankused = (blankfsstat.f_blocks - blankfsstat.f_bfree) * blankfsstat.f_bsize sys.stdout.write('\nWriting {0}: '.format(vol['mount'])) diff --git a/confluent_osdeploy/el9-diskless/profiles/default/scripts/installimage b/confluent_osdeploy/el9-diskless/profiles/default/scripts/installimage index 56597086..c461173b 100644 --- a/confluent_osdeploy/el9-diskless/profiles/default/scripts/installimage +++ b/confluent_osdeploy/el9-diskless/profiles/default/scripts/installimage @@ -41,6 +41,8 @@ echo "Port 22" >> /etc/ssh/sshd_config echo 'Match LocalPort 22' >> /etc/ssh/sshd_config echo ' ChrootDirectory /sysroot/run/imginst/targ' >> /etc/ssh/sshd_config kill -HUP $(cat /run/sshd.pid) +cp /sysroot/etc/pki/ca-trust/source/anchors/* /sysroot/run/imginst/targ/etc/pki/ca-trust/source/anchors/ +chroot /sysroot/run/imginst/targ update-ca-trust chroot /sysroot/run/imginst/targ bash -c "source /etc/confluent/functions; run_remote post.sh" chroot /sysroot bash -c "umount \$(tac /proc/mounts|awk '{print \$2}'|grep ^/run/imginst/targ)" diff --git a/confluent_osdeploy/el9-diskless/profiles/default/scripts/post.sh b/confluent_osdeploy/el9-diskless/profiles/default/scripts/post.sh index cb548bf4..914a12c3 100644 --- a/confluent_osdeploy/el9-diskless/profiles/default/scripts/post.sh +++ b/confluent_osdeploy/el9-diskless/profiles/default/scripts/post.sh @@ -2,7 +2,10 @@ # This script is executed 'chrooted' into a cloned disk target before rebooting # - +if [ -f /etc/dracut.conf.d/diskless.conf ]; then + rm /etc/dracut.conf.d/diskless.conf +fi +for kver in /lib/modules/*; do kver=$(basename $kver); kernel-install add $kver /boot/vmlinuz-$kver; done nodename=$(grep ^NODENAME /etc/confluent/confluent.info|awk '{print $2}') confluent_apikey=$(cat /etc/confluent/confluent.apikey) confluent_profile=$(grep ^profile: /etc/confluent/confluent.deploycfg|awk '{print $2}') @@ -16,6 +19,7 @@ if [[ "$confluent_mgr" == *:* ]]; then fi export nodename confluent_mgr confluent_profile confluent_websrv . /etc/confluent/functions +run_remote setupssh mkdir -p /var/log/confluent chmod 700 /var/log/confluent exec >> /var/log/confluent/confluent-post.log