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: