From 8a3fce85c0076e21f22304f0a45d44aa38f8959b Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Sun, 9 Aug 2026 07:05:49 +0200 Subject: [PATCH] Fix undefined names (F821) Every one of these raises NameError if its code path is reached: - nodeapply: run_automation accumulated into an exitcode that only existed in run(), so any automation error crashed instead of being reported. It now keeps and returns its own, tracked separately from the exit code of the ssh commands: the early exit after the spawn loop tests that one, and folding automation failures into it would exit with children already running and their pipes abandoned. Both are reported at the real exits. - nodeconsole: redraw() reads firstnodename, which was local to do_screenshot(); promote it to a module global like the other drawing state. - nodedeploy: the redeploy path appended to a lockednodes list that did not exist yet. The block that follows re-reads the same lock state and acts on it, so drop the dead duplicate. - samples/nodeattrib_from_switch.py, misc/filterpasswd: missing import sys. - xcc3: fixuuid was never imported. xcc imports xcc3, so take a local copy the way the smm handler does instead of creating an import cycle. - httpapi: the async session call still passed the WSGI-era env and an extra argument to handle_async(), which has taken only querydict since the aiohttp port. Calling it correctly exposed that handle_async() registers an AsyncSession before raising on the discontinued long poll path, so every request to it would leak a session that is never reaped. It now only creates one when there is a websocket handler to yield it to. - messages: the InputFirmwareUpdate.filename property checked self.filebynode[node] with no node in scope. __init__ already validates every expanded path and nodefile() rechecks per node, so drop the checks. - pam: drop the python2 branches referencing unicode and raw_input. The server has been python3 only since the asyncio port. - cooltera: the sensor-name listing referenced a nonexistent sensors dict. The available sensors depend on the model, which is only known after reading the device, so list them from the same status data the readings use. - deltapdu, eatonpdu, geist: the not-implemented response in update() used node outside the loop, unlike retrieve() in the same files and unlike raritan/enlogic. - confluentdbgcli: stray self. on a module-level socket connect. --- confluent_client/bin/nodeapply | 15 ++++++++++----- confluent_client/bin/nodeconsole | 2 ++ confluent_client/bin/nodedeploy | 5 ----- .../samples/nodeattrib_from_switch.py | 1 + confluent_server/confluent/asynchttp.py | 10 +++++----- .../confluent/discovery/handlers/xcc3.py | 16 ++++++++++++++++ confluent_server/confluent/httpapi.py | 4 +--- confluent_server/confluent/messages.py | 8 ++------ confluent_server/confluent/pam.py | 18 +++--------------- .../plugins/hardwaremanagement/cooltera.py | 13 ++++++++----- .../plugins/hardwaremanagement/deltapdu.py | 3 ++- .../plugins/hardwaremanagement/eatonpdu.py | 3 ++- .../plugins/hardwaremanagement/geist.py | 3 ++- confluent_server/confluentdbgcli.py | 2 +- misc/filterpasswd | 2 ++ 15 files changed, 57 insertions(+), 48 deletions(-) diff --git a/confluent_client/bin/nodeapply b/confluent_client/bin/nodeapply index 7547895f..6d619f57 100755 --- a/confluent_client/bin/nodeapply +++ b/confluent_client/bin/nodeapply @@ -37,6 +37,7 @@ import confluent.sortutil as sortutil devnull = None def run_automation(noderange, category, c): + exitcode = 0 automationbynode = {} for res in c.update('/noderange/{0}/deployment/remote_config/run'.format(noderange), { 'category': category, @@ -64,7 +65,8 @@ def run_automation(noderange, category, c): if res.get('complete', False): del automationbynode[node] sys.stdout.write('{0}: Automation complete\n'.format(node)) - + return exitcode + def run(): global devnull @@ -104,10 +106,13 @@ def run(): pipedesc = {} pendingexecs = deque() exitcode = 0 + # Kept apart from exitcode: a failed automation run must not trip the + # early exit below, which would abandon ssh children already spawned. + autoexitcode = 0 c.stop_if_noderange_over(args[0], options.maxnodes) if options.automation: - run_automation(args[0], options.automation, c) + autoexitcode = run_automation(args[0], options.automation, c) nodemap = {} cmdparms = [] @@ -124,7 +129,7 @@ def run(): cmdstorun.append(['run_remote', script]) if not cmdstorun: if options.automation: - sys.exit(0) + sys.exit(autoexitcode) argparser.print_help() sys.exit(1) for res in c.read('/noderange/{0}/nodes/'.format(args[0])): @@ -145,7 +150,7 @@ def run(): else: pendingexecs.append((sshnode, cmdv)) if not all or exitcode: - sys.exit(exitcode) + sys.exit(exitcode | autoexitcode) rdy = poller.poll(10) while all: pernodeout = {} @@ -193,7 +198,7 @@ def run(): sys.stdout.flush() if all: rdy = poller.poll(10) - sys.exit(exitcode) + sys.exit(exitcode | autoexitcode) def run_cmdv(node, cmdv, all, poller, pipedesc): diff --git a/confluent_client/bin/nodeconsole b/confluent_client/bin/nodeconsole index fe6decc9..800da71c 100755 --- a/confluent_client/bin/nodeconsole +++ b/confluent_client/bin/nodeconsole @@ -790,6 +790,7 @@ numrows = 0 cwidth = 0 cheight = 0 imagedatabynode = {} +firstnodename = None def redraw(): for node in imagedatabynode: @@ -818,6 +819,7 @@ async def do_screenshot(): global streaming global resized global numrows + global firstnodename sess = client.Command() if streaming: asyncio.create_task(watch_input()) diff --git a/confluent_client/bin/nodedeploy b/confluent_client/bin/nodedeploy index 47b607eb..e47f8d35 100755 --- a/confluent_client/bin/nodedeploy +++ b/confluent_client/bin/nodedeploy @@ -133,11 +133,6 @@ def main(args): curr = nodeinfo[attr].get('value', '') if curr and node not in profilebynode: profilebynode[node] = curr - for lockinfo in c.read('/noderange/{0}/deployment/lock'.format(args.noderange)): - for node in lockinfo.get('databynode', {}): - lockstate = lockinfo['databynode'][node]['lock']['value'] - if lockstate == 'locked': - lockednodes.append(node) if args.profile and profilebynode: sys.stderr.write('The -r/--redeploy option cannot be used with a profile, it redeploys the current or pending profile\n') return 1 diff --git a/confluent_client/samples/nodeattrib_from_switch.py b/confluent_client/samples/nodeattrib_from_switch.py index eff3e394..ba133846 100644 --- a/confluent_client/samples/nodeattrib_from_switch.py +++ b/confluent_client/samples/nodeattrib_from_switch.py @@ -13,6 +13,7 @@ import confluent.client as cl import socket import struct +import sys c = cl.Command() macs = [] interface = sys.argv[1] diff --git a/confluent_server/confluent/asynchttp.py b/confluent_server/confluent/asynchttp.py index 29c937d5..76458f8d 100644 --- a/confluent_server/confluent/asynchttp.py +++ b/confluent_server/confluent/asynchttp.py @@ -119,12 +119,12 @@ def handle_async(querydict, wshandler=None): # This may be one of two things, a request for a new async stream # or a request for next data from async stream # httpapi otherwise handles requests an injecting them to queue - if 'asyncid' not in querydict or not querydict['asyncid']: + if wshandler and ('asyncid' not in querydict or not querydict['asyncid']): # This is a new request, create a new multiplexer - currsess = AsyncSession(wshandler) - if wshandler: - yield currsess - return + yield AsyncSession(wshandler) + return + # Without a websocket handler there is nobody to hand a session to, so do + # not register one that would never be reaped. raise Exception("Long polling asynchttp is discontinued") diff --git a/confluent_server/confluent/discovery/handlers/xcc3.py b/confluent_server/confluent/discovery/handlers/xcc3.py index 0a1c7590..e2d57095 100644 --- a/confluent_server/confluent/discovery/handlers/xcc3.py +++ b/confluent_server/confluent/discovery/handlers/xcc3.py @@ -12,11 +12,27 @@ # See the License for the specific language governing permissions and # limitations under the License. +import codecs import confluent.discovery.handlers.redfishbmc as redfishbmc +import confluent.util as util import socket +import struct import aiohmi.util.webclient as webclient +# Duplicated from the xcc handler rather than imported: xcc imports this +# module, so importing it back would be circular. smm carries its own copy of +# this for the same reason. +def fixuuid(baduuid): + # SMM dumps it out in hex + uuidprefix = (baduuid[:8], baduuid[9:13], baduuid[14:18]) + a = codecs.encode(struct.pack('= (3,): - if isinstance(username, str): username = username.encode(encoding) - if isinstance(service, str): service = service.encode(encoding) - else: - if isinstance(username, unicode): - username = username.encode(encoding) - if isinstance(password, unicode): - password = password.encode(encoding) - if isinstance(service, unicode): - service = service.encode(encoding) + if isinstance(username, str): username = username.encode(encoding) + if isinstance(service, str): service = service.encode(encoding) if b'\x00' in username or b'\x00' in service: self.code = 4 # PAM_SYSTEM_ERR in Linux-PAM @@ -242,11 +234,7 @@ if __name__ == "__main__": readline.redisplay() readline.set_pre_input_hook(hook) - if sys.version_info >= (3,): - getinput = input - else: - getinput = raw_input - result = getinput(prompt) + result = input(prompt) readline.set_pre_input_hook() return result diff --git a/confluent_server/confluent/plugins/hardwaremanagement/cooltera.py b/confluent_server/confluent/plugins/hardwaremanagement/cooltera.py index 528d79c6..b5238577 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/cooltera.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/cooltera.py @@ -151,12 +151,9 @@ _sensors_by_node = {} async def read_sensors(element, node, configmanager): category, name = element[-2:] if len(element) == 3: - # just get names + # the request is for the names under a category, so that is the last + # element rather than the one before it category = name - name = 'all' - for sensor in sensors: - yield msg.ChildCollection(simplify_name(sensors[sensor][0])) - return if category in ('leds, fans'): return sn = _sensors_by_node.get(node, None) @@ -166,6 +163,12 @@ async def read_sensors(element, node, configmanager): statinfo = xml2stateinfo(statdata) _sensors_by_node[node] = (statinfo, time.time() + 1) sn = _sensors_by_node.get(node, None) + if len(element) == 3: + # the names are only known after reading the device, as the sensor + # set depends on the model + for sensor in sn[0] if sn else (): + yield msg.ChildCollection(simplify_name(sensor['name'])) + return if sn: yield msg.SensorReadings(sn[0], name=node) diff --git a/confluent_server/confluent/plugins/hardwaremanagement/deltapdu.py b/confluent_server/confluent/plugins/hardwaremanagement/deltapdu.py index e3da3786..02df6406 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/deltapdu.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/deltapdu.py @@ -195,7 +195,8 @@ async def retrieve(nodes, element, configmanager, inputdata): async def update(nodes, element, configmanager, inputdata): if 'outlets' not in element: - yield msg.ConfluentResourceUnavailable(node, 'Not implemented') + for node in nodes: + yield msg.ConfluentResourceUnavailable(node, 'Not implemented') return timeout = 4 for node in nodes: diff --git a/confluent_server/confluent/plugins/hardwaremanagement/eatonpdu.py b/confluent_server/confluent/plugins/hardwaremanagement/eatonpdu.py index f26bd0bd..f6c0a3ac 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/eatonpdu.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/eatonpdu.py @@ -327,7 +327,8 @@ async def retrieve(nodes, element, configmanager, inputdata): async def update(nodes, element, configmanager, inputdata): if 'outlets' not in element: - yield msg.ConfluentResourceUnavailable(node, 'Not implemented') + for node in nodes: + yield msg.ConfluentResourceUnavailable(node, 'Not implemented') return for node in nodes: gc = PDUClient(node, configmanager) diff --git a/confluent_server/confluent/plugins/hardwaremanagement/geist.py b/confluent_server/confluent/plugins/hardwaremanagement/geist.py index 9a3e0a8e..cb7bea1e 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/geist.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/geist.py @@ -336,7 +336,8 @@ async def retrieve(nodes, element, configmanager, inputdata): async def update(nodes, element, configmanager, inputdata): if 'outlets' not in element: - yield msg.ConfluentResourceUnavailable(node, 'Not implemented') + for node in nodes: + yield msg.ConfluentResourceUnavailable(node, 'Not implemented') return for node in nodes: gc = GeistClient(node, configmanager) diff --git a/confluent_server/confluentdbgcli.py b/confluent_server/confluentdbgcli.py index 6c804cc3..eb7d5f45 100644 --- a/confluent_server/confluentdbgcli.py +++ b/confluent_server/confluentdbgcli.py @@ -20,7 +20,7 @@ import readline import socket connection = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) -self.connection.connect('/var/run/confluent/dbg.sock') +connection.connect('/var/run/confluent/dbg.sock') readline.parse_and_bind("tab: complete") readline.parse_and_bind("set bell-style none") diff --git a/misc/filterpasswd b/misc/filterpasswd index 1d2785bd..11adc3a8 100644 --- a/misc/filterpasswd +++ b/misc/filterpasswd @@ -1,3 +1,5 @@ +import sys + uidmin = 1000 uidmax = 60000 gidmin = 1000