From 564230cf7e4a073ade2facc629705e4e8c48eb18 Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Fri, 14 Aug 2026 01:27:40 +0200 Subject: [PATCH] Give an unreachable target an error a user can read A console whose bmc had gone away reported "Unexpected error - None", and the api answered 504 with an error of None. The redfish plugin took the text for an unreachable target from the strerror of the socket error it caught, guarded by a hasattr that is always true: every OSError has a strerror attribute, and it is None on most of the ones a bmc going away produces, TimeoutError and gaierror among them. Ask for the text the same way as everywhere else instead, which also keeps the errno on the errors that do carry one. The same applies to an unreachable target raised with no message at all, so use the same helper there, on both transports. Underneath that, give the node error messages a default to fall back on rather than carrying whatever they were handed. Each subclass already had one, in an __init__ that an explicit None went straight past; making it a class attribute the base class applies means it holds however the message was built, and removes five copies of the same constructor. Also repair an affluent handler that put a closing parenthesis in the wrong place, passing its error text to Queue.put_nowait as a second argument. Any OSError there other than "no route to host" raised TypeError from inside the except clause instead of reporting anything. --- confluent_server/confluent/messages.py | 33 +++++++------------ .../plugins/hardwaremanagement/affluent.py | 6 ++-- .../plugins/hardwaremanagement/ipmi.py | 2 +- .../plugins/hardwaremanagement/redfish.py | 9 +++-- 4 files changed, 20 insertions(+), 30 deletions(-) diff --git a/confluent_server/confluent/messages.py b/confluent_server/confluent/messages.py index 161edfeb..1d792d9e 100644 --- a/confluent_server/confluent/messages.py +++ b/confluent_server/confluent/messages.py @@ -238,10 +238,14 @@ class ConfluentMessage(object): class ConfluentNodeError(object): apicode = 500 + # What to say when the caller had no text to offer. An error that reads + # "None" tells a user nothing, and every route out of here, the api body, + # the html and the exception strip_node raises, wants something to print. + defaulterror = 'Unknown error' - def __init__(self, node, errorstr): + def __init__(self, node, errorstr=None): self.node = node - self.error = errorstr + self.error = errorstr if errorstr else self.defaulterror def serialize(self): return msgpack.packb( @@ -267,10 +271,7 @@ class ConfluentNodeError(object): class NotImplemented(ConfluentNodeError): apicode = 501 - - def __init__(self, node, errorstr='Not implemented'): - self.node = node - self.error = errorstr + defaulterror = 'Not implemented' class Generic(ConfluentMessage): @@ -289,10 +290,7 @@ class Generic(ConfluentMessage): class ConfluentResourceUnavailable(ConfluentNodeError): apicode = 503 - - def __init__(self, node, errstr='Unavailable'): - self.node = node - self.error = errstr + defaulterror = 'Unavailable' def strip_node(self, node): raise exc.TargetResourceUnavailable() @@ -300,11 +298,7 @@ class ConfluentResourceUnavailable(ConfluentNodeError): class ConfluentTargetTimeout(ConfluentNodeError): apicode = 504 - - def __init__(self, node, errstr='timeout'): - self.node = node - self.error = errstr - + defaulterror = 'timeout' def strip_node(self, node): raise exc.TargetEndpointUnreachable(self.error) @@ -312,10 +306,7 @@ class ConfluentTargetTimeout(ConfluentNodeError): class ConfluentTargetNotFound(ConfluentNodeError): apicode = 404 - - def __init__(self, node, errorstr='not found'): - self.node = node - self.error = errorstr + defaulterror = 'not found' def strip_node(self, node): raise exc.NotFoundException(self.error) @@ -323,9 +314,7 @@ class ConfluentTargetNotFound(ConfluentNodeError): class ConfluentTargetInvalidCredentials(ConfluentNodeError): apicode = 502 - def __init__(self, node, errstr='bad credentials'): - self.node = node - self.error = errstr + defaulterror = 'bad credentials' def strip_node(self, node): raise exc.TargetEndpointBadCredentials diff --git a/confluent_server/confluent/plugins/hardwaremanagement/affluent.py b/confluent_server/confluent/plugins/hardwaremanagement/affluent.py index d7c4383f..5222c030 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/affluent.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/affluent.py @@ -46,13 +46,15 @@ class WebClient(object): 'pubkeys.tls_hardwaremanager attribute')) return {} except (socket.gaierror, socket.herror, TimeoutError) as e: - results.put_nowait(msg.ConfluentTargetTimeout(self.node, str(e))) + results.put_nowait( + msg.ConfluentTargetTimeout(self.node, exc.exc_text(e))) return {} except OSError as e: if e.errno == 113: results.put_nowait(msg.ConfluentTargetTimeout(self.node)) else: - results.put_nowait(msg.ConfluentTargetTimeout(self.node), str(e)) + results.put_nowait( + msg.ConfluentTargetTimeout(self.node, exc.exc_text(e))) return {} except Exception as e: results.put_nowait(msg.ConfluentNodeError(self.node, diff --git a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py index b7916362..db40e941 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py @@ -455,7 +455,7 @@ async def perform_request(operator, node, element, await results.put(msg.ConfluentNodeError(node, excmsg)) #raise except exc.TargetEndpointUnreachable as tu: - await results.put(msg.ConfluentTargetTimeout(node, str(tu))) + await results.put(msg.ConfluentTargetTimeout(node, exc_text(tu))) except ssl.SSLEOFError: await results.put(msg.ConfluentNodeError( node, 'Unable to communicate with the https server on ' diff --git a/confluent_server/confluent/plugins/hardwaremanagement/redfish.py b/confluent_server/confluent/plugins/hardwaremanagement/redfish.py index 89c5a13e..f3bdc132 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/redfish.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/redfish.py @@ -334,10 +334,9 @@ async def perform_request(operator, node, element, realop) return await ih.handle_request() except socket.error as se: - if hasattr(se, 'strerror'): - await results.put(msg.ConfluentTargetTimeout(node, se.strerror)) - else: - await results.put(msg.ConfluentTargetTimeout(node, str(se))) + # Every OSError has a strerror, but it is None on plenty of them, so + # ask for the text the same way as everywhere else rather than trust it + await results.put(msg.ConfluentTargetTimeout(node, exc_text(se))) except pygexc.IpmiException as ipmiexc: excmsg = exc_text(ipmiexc) if excmsg in ('Session no longer connected', 'timeout'): @@ -346,7 +345,7 @@ async def perform_request(operator, node, element, await results.put(msg.ConfluentNodeError(node, excmsg)) raise except exc.TargetEndpointUnreachable as tu: - await results.put(msg.ConfluentTargetTimeout(node, str(tu))) + await results.put(msg.ConfluentTargetTimeout(node, exc_text(tu))) except exc.TargetEndpointBadCredentials: await results.put(msg.ConfluentTargetInvalidCredentials(node)) except ssl.SSLEOFError: