From 5b132438f5676a911344c69454cf22cb22c7f7ee Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Sun, 16 Aug 2026 21:37:50 +0200 Subject: [PATCH 1/3] Let the api socket path be set rather than fixed _unixdomainhandler hardcoded /var/run/confluent/api.sock in four places and derived its directory from a fifth. It is now threaded through SockApi like the other bind settings, defaulting to the same path. That lets a service run on a temp socket as an ordinary user, which is what a test needs. --- confluent_server/confluent/sockapi.py | 36 ++++++++++++++++++--------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/confluent_server/confluent/sockapi.py b/confluent_server/confluent/sockapi.py index b3bcc289..d02bcce4 100644 --- a/confluent_server/confluent/sockapi.py +++ b/confluent_server/confluent/sockapi.py @@ -438,31 +438,40 @@ async def _tlsstartup(cnn): raise Exception('Unable to find workable SSL support') tasks.spawn(sessionhdl(cnn, authname, cert=cert)) -def removesocket(): +default_socketpath = "/var/run/confluent/api.sock" + + +def removesocket(socketpath=None): + if socketpath is None: + socketpath = default_socketpath try: - os.remove("/var/run/confluent/api.sock") + os.remove(socketpath) except OSError: pass -async def _unixdomainhandler(bind_group=None, bind_perms=None): +async def _unixdomainhandler(bind_group=None, bind_perms=None, + socketpath=None): aloop = asyncio.get_running_loop() if not bind_perms: bind_perms = 0o666 + if socketpath is None: + socketpath = default_socketpath unixsocket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) unixsocket.settimeout(0) try: - os.remove("/var/run/confluent/api.sock") + os.remove(socketpath) except OSError: # if file does not exist, no big deal pass - if not os.path.isdir("/var/run/confluent"): - os.makedirs('/var/run/confluent', 0o755) + socketdir = os.path.dirname(socketpath) + if socketdir and not os.path.isdir(socketdir): + os.makedirs(socketdir, 0o755) oldumask = os.umask(0o777 - bind_perms) - unixsocket.bind("/var/run/confluent/api.sock") - os.chmod("/var/run/confluent/api.sock", bind_perms) + unixsocket.bind(socketpath) + os.chmod(socketpath, bind_perms) if bind_group: - shutil.chown("/var/run/confluent/api.sock", group=bind_group) + shutil.chown(socketpath, group=bind_group) os.umask(oldumask) - atexit.register(removesocket) + atexit.register(removesocket, socketpath) unixsocket.listen(5) while True: cnn, addr = await aloop.sock_accept(unixsocket) @@ -491,13 +500,15 @@ async def _unixdomainhandler(bind_group=None, bind_perms=None): class SockApi(object): - def __init__(self, bindhost=None, bindport=None, bindgroup=None, bindperms=None): + def __init__(self, bindhost=None, bindport=None, bindgroup=None, + bindperms=None, socketpath=None): self.tlsserver = None self.unixdomainserver = None self.bind_host = bindhost or '::' self.bind_port = bindport or 13001 self.bind_group = bindgroup self.bind_perms = bindperms + self.socketpath = socketpath or default_socketpath async def start(self): global auditlog @@ -509,7 +520,8 @@ class SockApi(object): self.start_remoteapi() else: tasks.spawn(self.watch_for_cert()) - self.unixdomainserver = tasks.spawn_task(_unixdomainhandler(self.bind_group, self.bind_perms)) + self.unixdomainserver = tasks.spawn_task(_unixdomainhandler( + self.bind_group, self.bind_perms, self.socketpath)) async def watch_for_cert(self): watcher = libc.inotify_init1(os.O_NONBLOCK) From fd92209d4dd57c76d550322b029525871c7aa622 Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Sun, 16 Aug 2026 21:37:50 +0200 Subject: [PATCH 2/3] Read a port off the manager address over ipmi too The plugin connected to 623 whatever the address said, with a TODO in place of the parsing. It now reads one as the redfish plugin does, minus the brackets, which getaddrinfo rejects. IpmiConsole keys its endpoint mapping on host and port so several bmcs behind one address stay distinct, and unregisters only an entry it actually claimed. --- .../plugins/hardwaremanagement/ipmi.py | 52 +++++++++++++++---- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py index 1e28a187..90720295 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py @@ -262,14 +262,37 @@ def get_conn_params(node, configdata): kg = configdata['secret.ipmikg']['value'] else: kg = passphrase - # TODO(jbjohnso): check if the end has some number after a : without [] - # for non default port + # Read a port off the address, as the redfish plugin does. Brackets come + # off, unlike there: this address goes to socket.getaddrinfo, which rejects + # a bracketed literal, rather than into a URL where one is required. + bmc = bmc.strip() + port = 623 + if bmc.startswith('['): + bracket_end = bmc.find(']') + if bracket_end > 0: + if len(bmc) > bracket_end + 1 and bmc[bracket_end + 1] == ':': + try: + port = int(bmc[bracket_end + 2:]) + except (ValueError, TypeError): + pass + bmc = bmc[1:bracket_end] + elif bmc.count(':') == 1: + hostpart, _, portstr = bmc.rpartition(':') + try: + port = int(portstr) + except (ValueError, TypeError): + pass + bmc = hostpart + if not 0 < port <= 65535: + # NOTE: Fallback to 623 if port read is not valid + port = 623 + return { 'username': username, 'passphrase': passphrase, 'kg': kg, 'bmc': bmc, - 'port': 623, + 'port': port, } @@ -287,6 +310,11 @@ def _donothing(data): class IpmiConsole(conapi.Console): configattributes = frozenset(_configattributes) bmctonodemapping = {} + # Whether this instance is the one that put its endpoint in the mapping + # above. False until it does, so that an instance rejected as a duplicate, + # or one that failed earlier in __init__, does not remove on the way out an + # entry that belongs to the node holding the endpoint. + claimedbmc = False def __init__(self, node, config): self.error = None @@ -303,20 +331,26 @@ class IpmiConsole(conapi.Console): self.kg = connparams['kg'] self.bmc = connparams['bmc'] self.port = connparams['port'] + # The port is part of the identity: several bmcs may sit behind one + # address on different ports, and they are distinct devices. + self.bmckey = (self.bmc, self.port) self.connected = False - # ok, is self.bmc unique among nodes already + # ok, is this bmc unique among nodes already # Cannot actually create console until 'connect', when we get callback - if (self.bmc in self.bmctonodemapping and - self.bmctonodemapping[self.bmc] != node): + if (self.bmckey in self.bmctonodemapping and + self.bmctonodemapping[self.bmckey] != node): raise Exception( "Duplicate hardwaremanagement.manager attribute for {0} and {1}".format( - node, self.bmctonodemapping[self.bmc])) - self.bmctonodemapping[self.bmc] = node + node, self.bmctonodemapping[self.bmckey])) + self.bmctonodemapping[self.bmckey] = node + self.claimedbmc = True def __del__(self): self.solconnection = None + if not self.claimedbmc: + return try: - del self.bmctonodemapping[self.bmc] + del self.bmctonodemapping[self.bmckey] except KeyError: pass From eaa1a8bfc7c0f03dbe3f8a117f12a6f55b27bea6 Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Sun, 16 Aug 2026 21:37:50 +0200 Subject: [PATCH 3/3] Let a console work through a forwarded ipmi port A bmc behind a forward answers Activate Payload with the port it listens on itself, and the advertised-port check refused that, so a console failed where command traffic worked. The advertised port is never sent to, so the check now applies only on the default port. A bmc on another port advertising a third one is no longer refused outright, which nothing here could have served anyway. --- confluent_server/aiohmi/ipmi/console.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/confluent_server/aiohmi/ipmi/console.py b/confluent_server/aiohmi/ipmi/console.py index ab463eb5..4fdb49f0 100644 --- a/confluent_server/aiohmi/ipmi/console.py +++ b/confluent_server/aiohmi/ipmi/console.py @@ -148,7 +148,11 @@ class Console(object): # some BMCs disagree on the endianness, so do both valid_ports = (self.port, struct.unpack( 'H', self.port))[0]) - if (data[8] + (data[9] << 8)) not in valid_ports: + solport = data[8] + (data[9] << 8) + # A bmc behind a port forward answers with the port it listens on + # rather than the one it was reached through; payloads ride the + # session, never the advertised port. + if solport not in valid_ports and self.port == 623: # TODO(jbjohnso): support atypical SOL port number raise NotImplementedError("Non-standard SOL Port Number") # ignore data[10:11] for now, the vlan detail, shouldn't matter to this