From dda9b47a5192ba0228cc862c652d7119d12b1696 Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Fri, 14 Aug 2026 19:46:24 +0200 Subject: [PATCH] Ask a platform which firmware image types it takes Some will not take an image without being told which kind it is, and the only way to find out was to attempt an update and read the error, which writes to the bmc before it gets that far. --- confluent_client/bin/nodefirmware | 18 +++++++++++++- confluent_client/doc/man/nodefirmware.ronn | 22 +++++++++++++---- confluent_server/aiohmi/redfish/command.py | 9 +++++++ .../aiohmi/redfish/oem/ami/megarac.py | 24 ++++++++++++++++--- .../aiohmi/redfish/oem/generic.py | 10 ++++++++ confluent_server/confluent/core.py | 4 ++++ .../plugins/hardwaremanagement/ipmi.py | 4 ++++ .../plugins/hardwaremanagement/redfish.py | 6 +++++ 8 files changed, 88 insertions(+), 9 deletions(-) diff --git a/confluent_client/bin/nodefirmware b/confluent_client/bin/nodefirmware index 730e9ced..ad188071 100755 --- a/confluent_client/bin/nodefirmware +++ b/confluent_client/bin/nodefirmware @@ -56,7 +56,7 @@ components = ['all'] argparser = optparse.OptionParser( usage="Usage: " - "%prog [list][updatestatus][update [--backup] " + "%prog [list][updatestatus][updatetypes][update [--backup] " "[--parameterfile ] ]|[]") argparser.add_option('-b', '--backup', action='store_true', help='Target a backup bank rather than primary') @@ -71,6 +71,7 @@ argparser.add_option('-m', '--maxnodes', type='int', (options, args) = argparser.parse_args() upfile = None querystatus = False +querytypes = False try: noderange = args[0] if len(args) > 1: @@ -82,6 +83,8 @@ try: comps = args[2:] elif args[1] == 'updatestatus': querystatus = True + elif args[1] == 'updatetypes': + querytypes = True else: comps = args[1:] components = [] @@ -207,6 +210,17 @@ def show_firmware(session): ','.join(components))) +def show_update_types(session): + global exitcode + for res in session.read( + '/noderange/{0}/inventory/firmware/updatetypes'.format(noderange)): + exitcode |= client.printerror(res) + for node in res.get('databynode', {}): + types = res['databynode'][node].get('types', None) + if types: + print('{0}: {1}'.format(node, ','.join(types))) + + try: session = client.Command() if querystatus: @@ -217,6 +231,8 @@ try: currstat = res['databynode'][node].get('status', None) if currstat: print('{}: {}'.format(node, currstat)) + elif querytypes: + show_update_types(session) elif upfile is None: show_firmware(session) else: diff --git a/confluent_client/doc/man/nodefirmware.ronn b/confluent_client/doc/man/nodefirmware.ronn index 3fc3a018..cbf3a042 100644 --- a/confluent_client/doc/man/nodefirmware.ronn +++ b/confluent_client/doc/man/nodefirmware.ronn @@ -3,7 +3,7 @@ nodefirmware(8) -- Report firmware information on confluent nodes ## SYNOPSIS -`nodefirmware [list][updatestatus][update [--backup] [--parameterfile ] ]|[]` +`nodefirmware [list][updatestatus][updatetypes][update [--backup] [--parameterfile ] ]|[]` ## DESCRIPTION @@ -20,6 +20,10 @@ the FPGA version where applicable). The updatestatus argument will describe the state of firmware updates on the nodes. +The updatetypes argument will name the kinds of firmware image the platform has +to be told about, for use in a parameter file. Platforms that read the kind of +firmware from the image itself have none to name and say so. + In the update form, it accepts a single file and attempts to update it using the out of band facilities. Firmware updates can end in one of three states: @@ -57,13 +61,21 @@ holds, and refuse the update rather than guess. On an AMI MegaRAC that is an {"OemParameters": {"ImageType": "BIOS"}} -Attempting the update without it names the types the platform accepts in the -error, so there is no need to look them up first. The value is checked against -the same list before anything is uploaded, so a name the platform does not accept -is refused rather than sent. +`nodefirmware updatetypes` names the accepted types without +attempting an update. The value is also checked against the same list before +anything is uploaded, so a name the platform does not accept is refused rather +than sent. ## EXAMPLES +* Ask what kinds of firmware image a node has to be told about: +`# nodefirmware n1 updatetypes` +`n1: BMC,BIOS,MB_CPLD,SCM_CPLD,BPB_CPLD,HPM_BMC,HPM_BIOS,HPM_SCP,HPM_BIOS2` + +* Update the BIOS on a platform that has to be told: +`# echo '{"OemParameters": {"ImageType": "BIOS"}}' > /tmp/bios.json` +`# nodefirmware n1 update -p /tmp/bios.json /tmp/image.bin` + * Pull firmware from a node: `# nodefirmware r1` `r1: IMM: 3.70 (TCOO26H 2016-11-29T05:09:51)` diff --git a/confluent_server/aiohmi/redfish/command.py b/confluent_server/aiohmi/redfish/command.py index 73ee3281..868d3156 100644 --- a/confluent_server/aiohmi/redfish/command.py +++ b/confluent_server/aiohmi/redfish/command.py @@ -1811,6 +1811,15 @@ class Command(object): oem = await self.oem() return await oem.get_update_status() + async def get_update_types(self): + """The kinds of firmware image this platform has to be told about + + Only some implementations will not take an image without being told + which kind it is, and those are the ones with an answer here. + """ + oem = await self.oem() + return await oem.get_update_types(self) + async def update_firmware(self, file, data=None, progress=None, bank=None, otherfields=()): """Send file to BMC to perform firmware update diff --git a/confluent_server/aiohmi/redfish/oem/ami/megarac.py b/confluent_server/aiohmi/redfish/oem/ami/megarac.py index a74c9b1f..4a640296 100644 --- a/confluent_server/aiohmi/redfish/oem/ami/megarac.py +++ b/confluent_server/aiohmi/redfish/oem/ami/megarac.py @@ -108,14 +108,32 @@ class OEMHandler(generic.OEMHandler): 'PreserveConfiguration': preserve, }}}, method='PATCH', etag='*') + # What the parameter naming the kind of firmware is called in the action + # info. Builds differ, and the names it takes are the same vocabulary as + # the OemParameters ImageType a multipart push wants. + _imagetypeparams = ('ImageType', 'UpdateComponent') + async def _allowed_imagetypes(self): - actinfo = await self._do_web_request( - '/redfish/v1/UpdateService/SimpleUpdateActionInfo') + try: + actinfo = await self._do_web_request( + '/redfish/v1/UpdateService/SimpleUpdateActionInfo') + except pygexc.PyghmiException: + # A build that does not publish the action info still wants an + # image type, it just cannot say which names it takes + return [] for param in actinfo.get('Parameters', []): - if param.get('Name', None) == 'ImageType': + if param.get('Name', None) in self._imagetypeparams: return param.get('AllowableValues', []) return [] + async def get_update_types(self, fishclient): + allowed = await self._allowed_imagetypes() + if not allowed: + raise pygexc.UnsupportedFunctionality( + 'This bmc has to be told what kind of firmware an image holds ' + 'but does not publish the names it accepts') + return allowed + async def _checked_imagetype(self, imagetype, filename): """Check the image type the parameter file gave against what is accepted. diff --git a/confluent_server/aiohmi/redfish/oem/generic.py b/confluent_server/aiohmi/redfish/oem/generic.py index a710a833..942b1ffa 100644 --- a/confluent_server/aiohmi/redfish/oem/generic.py +++ b/confluent_server/aiohmi/redfish/oem/generic.py @@ -1525,6 +1525,16 @@ class OEMHandler(object): msgs = response.get('Messages', []) return ';'.join(self.format_message(x) for x in msgs) + async def get_update_types(self, fishclient): + """The kinds of firmware image this platform has to be told about. + + A platform that reads the kind from the image has none to report, which + is not the same as failing to answer. + """ + raise exc.UnsupportedFunctionality( + 'This platform does not take a firmware image type, it reads the ' + 'kind of firmware from the image') + async def update_firmware(self, filename, data=None, progress=None, bank=None, otherfields=()): # disable cache to make sure we trigger the token renewal logic if needed usd, upurl, ismultipart = await self.retrieve_firmware_upload_url() diff --git a/confluent_server/confluent/core.py b/confluent_server/confluent/core.py index 0c4491ca..82da37f7 100644 --- a/confluent_server/confluent/core.py +++ b/confluent_server/confluent/core.py @@ -557,6 +557,10 @@ def _init_core(): 'pluginattrs': ['hardwaremanagement.method'], 'default': 'null', }), + 'updatetypes': PluginRoute({ + 'pluginattrs': ['hardwaremanagement.method'], + 'default': 'null', + }), 'updates': { 'active': PluginCollection({ 'pluginattrs': ['hardwaremanagement.method'], diff --git a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py index e45e0d59..6cde2fed 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py @@ -604,6 +604,10 @@ class IpmiHandler: await self.handle_update() elif self.element[:3] == ['inventory', 'firmware', 'updatestatus']: await self.handle_update_status() + elif self.element[:3] == ['inventory', 'firmware', 'updatetypes']: + # Otherwise this falls through to the inventory handler and is read + # as the name of a firmware component + raise self._unsupported() elif self.element[0] == 'inventory': await self.handle_inventory() elif self.element == ['media', 'attach']: diff --git a/confluent_server/confluent/plugins/hardwaremanagement/redfish.py b/confluent_server/confluent/plugins/hardwaremanagement/redfish.py index 8cc84492..9311fac4 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/redfish.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/redfish.py @@ -477,6 +477,8 @@ class IpmiHandler: await self.handle_update() elif self.element[:3] == ['inventory', 'firmware', 'updatestatus']: await self.handle_update_status() + elif self.element[:3] == ['inventory', 'firmware', 'updatetypes']: + await self.handle_update_types() elif self.element[0] == 'inventory': await self.handle_inventory() elif self.element == ['media', 'attach']: @@ -923,6 +925,10 @@ class IpmiHandler: status = await self.ipmicmd.get_update_status() await self.output.put(msg.KeyValueData({'status': status}, self.node)) + async def handle_update_types(self): + await self.output.put(msg.KeyValueData( + {'types': await self.ipmicmd.get_update_types()}, self.node)) + async def handle_inventory(self): if self.element[1] == 'firmware': if len(self.element) == 3: