Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-pyghmi for openSUSE:Factory 
checked in at 2024-01-05 21:41:36
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-pyghmi (Old)
 and      /work/SRC/openSUSE:Factory/.python-pyghmi.new.28375 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-pyghmi"

Fri Jan  5 21:41:36 2024 rev:20 rq:1136723 version:1.5.63

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-pyghmi/python-pyghmi.changes      
2023-06-22 23:25:12.177627882 +0200
+++ /work/SRC/openSUSE:Factory/.python-pyghmi.new.28375/python-pyghmi.changes   
2024-01-05 21:42:44.937210285 +0100
@@ -1,0 +2,24 @@
+Thu Jan  4 09:06:54 UTC 2024 - Dirk Müller <[email protected]>
+
+- update to 1.5.63:
+  * Provide generic redfish push update support
+  * Return empty tuple rather than None
+  * Expect timeout on reseat
+  * Reset seek on unwrapped zip
+  * Prefer first PCI device id
+  * Provide error checking using XCC hints
+  * Handle unseekable data
+  * Fix capitilazation consistency
+  * Trigger unwrapping only with singular update
+  * Apply uxz payload rather than whole zip to XCC
+  * Implement XCC override for health in redfish
+  * Avoid error on unexpected shutdown
+  * Add missing class to generic redfish support
+  * Correct missing lookup in generic OEM redfish
+  * Prepare redfish for OEM health
+  * Add deduplicated events and let XCC events control entirely
+  * Fix compatibility with python2
+  * Clear any logonwaiters on broken
+  * Handle non-numeric with 0 number format
+
+-------------------------------------------------------------------

Old:
----
  pyghmi-1.5.61.tar.gz

New:
----
  pyghmi-1.5.63.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-pyghmi.spec ++++++
--- /var/tmp/diff_new_pack.pJY1GX/_old  2024-01-05 21:42:45.529231932 +0100
+++ /var/tmp/diff_new_pack.pJY1GX/_new  2024-01-05 21:42:45.529231932 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package python-pyghmi
 #
-# Copyright (c) 2023 SUSE LLC
+# Copyright (c) 2024 SUSE LLC
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -17,13 +17,13 @@
 
 
 Name:           python-pyghmi
-Version:        1.5.61
+Version:        1.5.63
 Release:        0
 Summary:        General Hardware Management Initiative (IPMI and others)
 License:        Apache-2.0
 Group:          Development/Languages/Python
 URL:            https://docs.openstack.org/pyghmi
-Source0:        
https://files.pythonhosted.org/packages/source/p/pyghmi/pyghmi-1.5.61.tar.gz
+Source0:        
https://files.pythonhosted.org/packages/source/p/pyghmi/pyghmi-1.5.63.tar.gz
 BuildRequires:  openstack-macros
 BuildRequires:  python3-cryptography >= 2.1
 BuildRequires:  python3-devel
@@ -73,7 +73,7 @@
 
 %build
 %{py3_build}
-PYTHONPATH=. PBR_VERSION=1.5.61 %sphinx_build -b html doc/source doc/build/html
+PYTHONPATH=. PBR_VERSION=1.5.63 %sphinx_build -b html doc/source doc/build/html
 rm -rf doc/build/html/.{doctrees,buildinfo}
 
 %install

++++++ _service ++++++
--- /var/tmp/diff_new_pack.pJY1GX/_old  2024-01-05 21:42:45.553232809 +0100
+++ /var/tmp/diff_new_pack.pJY1GX/_new  2024-01-05 21:42:45.557232956 +0100
@@ -1,12 +1,12 @@
 <services>
-  <service mode="disabled" name="renderspec">
+  <service mode="manual" name="renderspec">
     <param 
name="input-template">https://opendev.org/openstack/rpm-packaging/raw/master/openstack/pyghmi/pyghmi.spec.j2</param>
     <param name="output-name">python-pyghmi.spec</param>
     <param 
name="requirements">https://opendev.org/x/pyghmi/raw/master/requirements.txt</param>
     <param name="changelog-email">[email protected]</param>
   </service>
-  <service mode="disabled" name="download_files">
+  <service mode="manual" name="download_files">
   </service>
-  <service name="format_spec_file" mode="disabled"/>
+  <service name="format_spec_file" mode="manual"/>
 </services>
 

++++++ pyghmi-1.5.61.tar.gz -> pyghmi-1.5.63.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyghmi-1.5.61/ChangeLog new/pyghmi-1.5.63/ChangeLog
--- old/pyghmi-1.5.61/ChangeLog 2023-04-13 14:58:47.000000000 +0200
+++ new/pyghmi-1.5.63/ChangeLog 2023-08-28 16:40:08.000000000 +0200
@@ -1,6 +1,33 @@
 CHANGES
 =======
 
+1.5.63
+------
+
+* Provide generic redfish push update support
+* Return empty tuple rather than None
+
+1.5.62
+------
+
+* Expect timeout on reseat
+* Reset seek on unwrapped zip
+* Prefer first PCI device id
+* Provide error checking using XCC hints
+* Handle unseekable data
+* Fix capitilazation consistency
+* Trigger unwrapping only with singular update
+* Apply uxz payload rather than whole zip to XCC
+* Implement XCC override for health in redfish
+* Avoid error on unexpected shutdown
+* Add missing class to generic redfish support
+* Correct missing lookup in generic OEM redfish
+* Prepare redfish for OEM health
+* Add deduplicated events and let XCC events control entirely
+* Fix compatibility with python2
+* Clear any logonwaiters on broken
+* Handle non-numeric with 0 number format
+
 1.5.61
 ------
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyghmi-1.5.61/PKG-INFO new/pyghmi-1.5.63/PKG-INFO
--- old/pyghmi-1.5.61/PKG-INFO  2023-04-13 14:58:48.335039900 +0200
+++ new/pyghmi-1.5.63/PKG-INFO  2023-08-28 16:40:08.964266500 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: pyghmi
-Version: 1.5.61
+Version: 1.5.63
 Summary: Python General Hardware Management Initiative (IPMI and others)
 Home-page: http://github.com/openstack/pyghmi/
 Author: Jarrod Johnson
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyghmi-1.5.61/pyghmi/ipmi/command.py 
new/pyghmi-1.5.63/pyghmi/ipmi/command.py
--- old/pyghmi-1.5.61/pyghmi/ipmi/command.py    2023-04-13 14:58:19.000000000 
+0200
+++ new/pyghmi-1.5.63/pyghmi/ipmi/command.py    2023-08-28 16:39:39.000000000 
+0200
@@ -907,7 +907,7 @@
         ip6gd = bytearray(ip6g['data'])
         if ip6gd[0] != 0x11:
             raise Exception('Unsupported reply')
-        gwa = socket.inet_ntop(socket.AF_INET6, ip6gd[1:17])
+        gwa = socket.inet_ntop(socket.AF_INET6, bytes(ip6gd[1:17]))
         retdata['static_gateway'] = gwa
         return retdata
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyghmi-1.5.61/pyghmi/ipmi/oem/lenovo/config.py 
new/pyghmi-1.5.63/pyghmi/ipmi/oem/lenovo/config.py
--- old/pyghmi-1.5.61/pyghmi/ipmi/oem/lenovo/config.py  2023-04-13 
14:58:19.000000000 +0200
+++ new/pyghmi-1.5.63/pyghmi/ipmi/oem/lenovo/config.py  2023-08-28 
16:39:39.000000000 +0200
@@ -353,6 +353,7 @@
                     reset = False
                     name = setting.find("mriName").text
                     help = setting.find("desc").text
+                    validexpression = None
                     onedata = setting.find('text_data')
                     if onedata is not None:
                         if onedata.get('password') == 'true':
@@ -366,6 +367,7 @@
                     if onedata is not None:
                         if onedata.get('maxinstance') is not None:
                             forceinstance = True
+                        validexpression = onedata.get('pattern', None)
                         instances = list(onedata.iter('instance'))
                         if not instances:
                             protect = True  # not supported yet
@@ -451,6 +453,7 @@
                                     readonly_expression=readonly,
                                     hide_expression=hide,
                                     sortid=sortid,
+                                    validexpression=validexpression,
                                     alias=alias)
                                 sortid += 1
                                 instidx += 1
@@ -479,6 +482,7 @@
                                 readonly_expression=readonly,
                                 hide_expression=hide,
                                 sortid=sortid,
+                                validexpression=validexpression,
                                 alias=alias)
                             sortid += 1
                         continue
@@ -503,6 +507,7 @@
                                                readonly_expression=readonly,
                                                hide_expression=hide,
                                                sortid=sortid,
+                                               validexpression=validexpression,
                                                alias=alias)
                     sortid = sortid + 1
         for opt in options:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyghmi-1.5.61/pyghmi/ipmi/oem/lenovo/handler.py 
new/pyghmi-1.5.63/pyghmi/ipmi/oem/lenovo/handler.py
--- old/pyghmi-1.5.61/pyghmi/ipmi/oem/lenovo/handler.py 2023-04-13 
14:58:19.000000000 +0200
+++ new/pyghmi-1.5.63/pyghmi/ipmi/oem/lenovo/handler.py 2023-08-28 
16:39:39.000000000 +0200
@@ -346,7 +346,7 @@
             return self.smmhandler.get_ntp_servers()
         if self.has_tsma:
             return self.tsmahandler.get_ntp_servers()
-        return None
+        return ()
 
     def set_ntp_enabled(self, enabled):
         if self.has_tsm or self.has_ami or self.has_asrock:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyghmi-1.5.61/pyghmi/ipmi/oem/lenovo/imm.py 
new/pyghmi-1.5.63/pyghmi/ipmi/oem/lenovo/imm.py
--- old/pyghmi-1.5.61/pyghmi/ipmi/oem/lenovo/imm.py     2023-04-13 
14:58:19.000000000 +0200
+++ new/pyghmi-1.5.63/pyghmi/ipmi/oem/lenovo/imm.py     2023-08-28 
16:39:39.000000000 +0200
@@ -1,5 +1,5 @@
 # coding=utf8
-# Copyright 2016-2019 Lenovo
+# Copyright 2016-2023 Lenovo
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -28,6 +28,7 @@
 import weakref
 
 import six
+import zipfile
 
 import pyghmi.constants as pygconst
 import pyghmi.exceptions as pygexc
@@ -314,6 +315,11 @@
                                 '({2})'.format(
                                     newvalue, key,
                                     ','.join(self.fwo[key]['possible'])))
+                elif self.fwo[key]['validexpression']:
+                    if not re.match(self.fwo[key]['validexpression'], 
newvalue):
+                        raise pygexc.InvalidParameterValue(
+                            '"{0}" does not match expression "{1}"'.format(
+                                newvalue, self.fwo[key]['validexpression']))
                 newnewvalues.append(newvalue)
             if len(newnewvalues) == 1:
                 self.fwo[key]['new_value'] = newnewvalues[0]
@@ -790,7 +796,7 @@
                     if venid is not None:
                         bdata['PCI Vendor ID'] = '{0:04x}'.format(venid)
                     devid = fundata.get(self.ADP_DEVID, None)
-                    if devid is not None:
+                    if devid is not None and 'PCI Device ID' not in bdata:
                         bdata['PCI Device ID'] = '{0:04x}'.format(devid)
                     venid = fundata.get(self.ADP_SUBVENID, None)
                     if venid is not None:
@@ -933,8 +939,11 @@
                 {'RoleId': role}, method='PATCH')
 
     def reseat(self):
-        rsp = self.wc.grab_json_response_with_status(
+        wc = self.wc.dupe(timeout=5)
+        rsp = wc.grab_json_response_with_status(
             '/api/providers/virt_reseat', '{}')
+        if rsp[1] == 500 and rsp[0] == 'Target Unavailable':
+            return
         if rsp[1] != 200 or rsp[0].get('return', 1) != 0:
             raise pygexc.UnsupportedFunctionality(
                 'This platform does not support AC reseat.')
@@ -1985,6 +1994,29 @@
         if usd['HttpPushUriTargetsBusy']:
             raise pygexc.TemporaryError('Cannot run multiple updates to same '
                                         'target concurrently')
+        z = None
+        wrappedfilename = None
+        uxzcount = 0
+        needseek = False
+        if data and hasattr(data, 'read'):
+            if zipfile.is_zipfile(data):
+                needseek = True
+                z = zipfile.ZipFile(data)
+            else:
+                data.seek(0)
+        elif data is None and zipfile.is_zipfile(filename):
+            z = zipfile.ZipFile(filename)
+        if z:
+            for tmpname in z.namelist():
+                if tmpname.startswith('payloads/'):
+                    uxzcount += 1
+                    if tmpname.endswith('.uxz'):
+                        wrappedfilename = tmpname
+        if uxzcount == 1 and wrappedfilename:
+            filename = os.path.basename(wrappedfilename)
+            data = z.open(wrappedfilename)
+        elif needseek:
+            data.seek(0)
         upurl = usd['HttpPushUri']
         self.grab_redfish_response_with_status(
             '/redfish/v1/UpdateService',
@@ -2339,6 +2371,7 @@
             'W': pygconst.Health.Warning,
         }
         infoevents = False
+        existingevts = set([])
         for item in rsp.get('items', ()):
             # while usually the ipmi interrogation shall explain things,
             # just in case there is a gap, make sure at least the
@@ -2353,14 +2386,14 @@
             if item['cmnid'] == 'FQXSPPW0104J':
                 # This event does not get modeled by the sensors
                 # add a made up sensor to explain
-                summary['badreadings'].append(
+                fallbackdata.append(
                     sdr.SensorReading({'name': item['source'],
                                        'states': ['Not Redundant'],
                                        'state_ids': [3],
                                        'health': pygconst.Health.Warning,
                                        'type': 'Power'}, ''))
             elif item['cmnid'] == 'FQXSFMA0041K':
-                summary['badreadings'].append(
+                fallbackdata.append(
                     sdr.SensorReading({
                         'name': 'Optane DCPDIMM',
                         'health': pygconst.Health.Warning,
@@ -2369,6 +2402,10 @@
                         '')
                 )
             else:
+                currevt = '{}:{}'.format(item['source'], item['message'])
+                if currevt in existingevts:
+                    continue
+                existingevts.add(currevt)
                 fallbackdata.append(sdr.SensorReading({
                     'name': item['source'],
                     'states': [item['message']],
@@ -2387,6 +2424,9 @@
                     'health': pygconst.Health.Warning,
                     'type': 'LED',
                 }, ''))
+        summary['badreadings'] = fallbackdata
+        if fallbackdata:
+            raise pygexc.BypassGenericBehavior()
         return fallbackdata
         # Will use the generic handling for unhealthy systems
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyghmi-1.5.61/pyghmi/ipmi/oem/lenovo/nextscale.py 
new/pyghmi-1.5.63/pyghmi/ipmi/oem/lenovo/nextscale.py
--- old/pyghmi-1.5.61/pyghmi/ipmi/oem/lenovo/nextscale.py       2023-04-13 
14:58:19.000000000 +0200
+++ new/pyghmi-1.5.63/pyghmi/ipmi/oem/lenovo/nextscale.py       2023-08-28 
16:39:39.000000000 +0200
@@ -1028,11 +1028,18 @@
         rsp = wc.getresponse()
         rsp.read()
         complete = False
+        tries = 0
         while not complete:
             ipmisession.Session.pause(3)
             wc.request('POST', '/data', 'get=fwProgress,fwUpdate')
-            rsp = wc.getresponse()
-            progdata = rsp.read()
+            try:
+                rsp = wc.getresponse()
+                progdata = rsp.read()
+            except Exception:
+                if tries > 2:
+                     break
+                tries += 1
+                continue
             if rsp.status != 200:
                 raise Exception('Error applying firmware')
             progdata = fromstring(progdata)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyghmi-1.5.61/pyghmi/ipmi/private/session.py 
new/pyghmi-1.5.63/pyghmi/ipmi/private/session.py
--- old/pyghmi-1.5.61/pyghmi/ipmi/private/session.py    2023-04-13 
14:58:19.000000000 +0200
+++ new/pyghmi-1.5.63/pyghmi/ipmi/private/session.py    2023-08-28 
16:39:39.000000000 +0200
@@ -583,6 +583,12 @@
             self.broken = True
             if self.socket:
                 self.socketpool[self.socket] -= 1
+        while self.logonwaiters:
+            waiter = self.logonwaiters.pop()
+            try:
+                waiter({'error': 'Session failed to initalize'})
+            except Exception:
+                pass
 
     def onlogon(self, parameter):
         if 'error' in parameter:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyghmi-1.5.61/pyghmi/ipmi/sdr.py 
new/pyghmi-1.5.63/pyghmi/ipmi/sdr.py
--- old/pyghmi-1.5.61/pyghmi/ipmi/sdr.py        2023-04-13 14:58:19.000000000 
+0200
+++ new/pyghmi-1.5.63/pyghmi/ipmi/sdr.py        2023-08-28 16:39:39.000000000 
+0200
@@ -372,6 +372,7 @@
         # event only, compact and full are very similar
         # this function handles the common aspects of compact and full
         # offsets from spec, minus 6
+        self.has_thresholds = False
         self.sensor_owner = entry[0]
         self.sensor_lun = entry[1] & 0x03
         self.sensor_number = entry[2]
@@ -383,6 +384,8 @@
         else:
             self.sensor_type_number = entry[7]
             self.reading_type = entry[8]  # table 42-1
+        if self.rectype == 1 and entry[6] & 0b00001100:
+            self.has_thresholds = True
         try:
             self.sensor_type = self.event_consts.sensor_type_codes[
                 self.sensor_type_number]
@@ -487,7 +490,7 @@
             numeric = twos_complement(reading[0], 8)
         elif self.numeric_format == 1:
             numeric = ones_complement(reading[0], 8)
-        elif self.numeric_format == 0:
+        elif self.numeric_format == 0 and (self.has_thresholds or 
self.reading_type == 1):
             numeric = reading[0]
         discrete = True
         if numeric is not None:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyghmi-1.5.61/pyghmi/redfish/command.py 
new/pyghmi-1.5.63/pyghmi/redfish/command.py
--- old/pyghmi-1.5.61/pyghmi/redfish/command.py 2023-04-13 14:58:19.000000000 
+0200
+++ new/pyghmi-1.5.63/pyghmi/redfish/command.py 2023-08-28 16:39:39.000000000 
+0200
@@ -765,72 +765,7 @@
         return {'identifystate': self._idstatemap[ledstate]}
 
     def get_health(self, verbose=True):
-        health = self.sysinfo.get('Status', {})
-        health = health.get('HealthRollup', health.get('Health', 'Unknown'))
-        warnunknown = health == 'Unknown'
-        health = _healthmap[health]
-        summary = {'badreadings': [], 'health': health}
-        if health > 0 and verbose:
-            # now have to manually peruse all psus, fans, processors, ram,
-            # storage
-            procsumstatus = self.sysinfo.get('ProcessorSummary', {}).get(
-                'Status', {})
-            procsumstatus = procsumstatus.get('HealthRollup',
-                                              procsumstatus.get('Health',
-                                                                None))
-            if procsumstatus != 'OK':
-                procfound = False
-                procurl = self.sysinfo.get('Processors', {}).get('@odata.id',
-                                                                 None)
-                if procurl:
-                    for cpu in self._do_web_request(procurl).get(
-                            'Members', []):
-                        cinfo = self._do_web_request(cpu['@odata.id'])
-                        if cinfo.get('Status', {}).get(
-                                'State', None) == 'Absent':
-                            continue
-                        if cinfo.get('Status', {}).get(
-                                'Health', None) not in ('OK', None):
-                            procfound = True
-                            summary['badreadings'].append(SensorReading(cinfo))
-                if not procfound:
-                    procinfo = self.sysinfo['ProcessorSummary']
-                    procinfo['Name'] = 'Processors'
-                    summary['badreadings'].append(SensorReading(procinfo))
-            memsumstatus = self.sysinfo.get(
-                'MemorySummary', {}).get('Status', {})
-            memsumstatus = memsumstatus.get('HealthRollup',
-                                            memsumstatus.get('Health', None))
-            if memsumstatus != 'OK':
-                dimmfound = False
-                for mem in self._do_web_request(
-                        self.sysinfo['Memory']['@odata.id'])['Members']:
-                    dimminfo = self._do_web_request(mem['@odata.id'])
-                    if dimminfo.get('Status', {}).get(
-                            'State', None) == 'Absent':
-                        continue
-                    if dimminfo.get('Status', {}).get(
-                            'Health', None) not in ('OK', None):
-                        summary['badreadings'].append(SensorReading(dimminfo))
-                        dimmfound = True
-                if not dimmfound:
-                    meminfo = self.sysinfo['MemorySummary']
-                    meminfo['Name'] = 'Memory'
-                    summary['badreadings'].append(SensorReading(meminfo))
-            for adapter in self.sysinfo['PCIeDevices']:
-                adpinfo = self._do_web_request(adapter['@odata.id'])
-                if adpinfo['Status']['Health'] not in ('OK', None):
-                    summary['badreadings'].append(SensorReading(adpinfo))
-            for fun in self.sysinfo['PCIeFunctions']:
-                funinfo = self._do_web_request(fun['@odata.id'])
-                if funinfo['Status']['Health'] not in ('OK', None):
-                    summary['badreadings'].append(SensorReading(funinfo))
-        if warnunknown and not summary['badreadings']:
-            unkinf = SensorReading({'Name': 'BMC',
-                                    'Status': {'Health': 'Unknown'}})
-            unkinf.states = ['System does not provide health information']
-            summary['badreadings'].append(unkinf)
-        return summary
+        return self.oem.get_health(self, verbose)
 
     def get_bmc_configuration(self):
         """Get miscellaneous BMC configuration
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyghmi-1.5.61/pyghmi/redfish/oem/generic.py 
new/pyghmi-1.5.63/pyghmi/redfish/oem/generic.py
--- old/pyghmi-1.5.61/pyghmi/redfish/oem/generic.py     2023-04-13 
14:58:19.000000000 +0200
+++ new/pyghmi-1.5.63/pyghmi/redfish/oem/generic.py     2023-08-28 
16:39:39.000000000 +0200
@@ -16,9 +16,39 @@
 import json
 import os
 import re
+import time
 
+import pyghmi.constants as const
 import pyghmi.exceptions as exc
 import pyghmi.media as media
+import pyghmi.util.webclient as webclient
+
+
+class SensorReading(object):
+    def __init__(self, healthinfo, sensor=None, value=None, units=None,
+                 unavailable=False):
+        if sensor:
+            self.name = sensor['name']
+        else:
+            self.name = healthinfo['Name']
+            self.health = _healthmap.get(healthinfo.get(
+                'Status', {}).get('Health', None), const.Health.Warning)
+            self.states = [healthinfo.get('Status', {}).get('Health',
+                                                            'Unknown')]
+            self.health = _healthmap[healthinfo['Status']['Health']]
+            self.states = [healthinfo['Status']['Health']]
+        self.value = value
+        self.state_ids = None
+        self.imprecision = None
+        self.units = units
+        self.unavailable = unavailable
+
+_healthmap = {
+    'Critical': const.Health.Critical,
+    'Unknown': const.Health.Warning,
+    'Warning': const.Health.Warning,
+    'OK': const.Health.Ok,
+}
 
 boot_devices_write = {
     'net': 'Pxe',
@@ -133,6 +163,74 @@
         self._urlcache = cache
         self.webclient = webclient
 
+    def get_health(self, fishclient, verbose=True):
+        health = fishclient.sysinfo.get('Status', {})
+        health = health.get('HealthRollup', health.get('Health', 'Unknown'))
+        warnunknown = health == 'Unknown'
+        health = _healthmap[health]
+        summary = {'badreadings': [], 'health': health}
+        if health > 0 and verbose:
+            # now have to manually peruse all psus, fans, processors, ram,
+            # storage
+            procsumstatus = fishclient.sysinfo.get('ProcessorSummary', {}).get(
+                'Status', {})
+            procsumstatus = procsumstatus.get('HealthRollup',
+                                              procsumstatus.get('Health',
+                                                                None))
+            if procsumstatus != 'OK':
+                procfound = False
+                procurl = fishclient.sysinfo.get('Processors', 
{}).get('@odata.id',
+                                                                 None)
+                if procurl:
+                    for cpu in fishclient._do_web_request(procurl).get(
+                            'Members', []):
+                        cinfo = fishclient._do_web_request(cpu['@odata.id'])
+                        if cinfo.get('Status', {}).get(
+                                'State', None) == 'Absent':
+                            continue
+                        if cinfo.get('Status', {}).get(
+                                'Health', None) not in ('OK', None):
+                            procfound = True
+                            summary['badreadings'].append(SensorReading(cinfo))
+                if not procfound:
+                    procinfo = fishclient.sysinfo['ProcessorSummary']
+                    procinfo['Name'] = 'Processors'
+                    summary['badreadings'].append(SensorReading(procinfo))
+            memsumstatus = fishclient.sysinfo.get(
+                'MemorySummary', {}).get('Status', {})
+            memsumstatus = memsumstatus.get('HealthRollup',
+                                            memsumstatus.get('Health', None))
+            if memsumstatus != 'OK':
+                dimmfound = False
+                for mem in fishclient._do_web_request(
+                        fishclient.sysinfo['Memory']['@odata.id'])['Members']:
+                    dimminfo = fishclient._do_web_request(mem['@odata.id'])
+                    if dimminfo.get('Status', {}).get(
+                            'State', None) == 'Absent':
+                        continue
+                    if dimminfo.get('Status', {}).get(
+                            'Health', None) not in ('OK', None):
+                        summary['badreadings'].append(SensorReading(dimminfo))
+                        dimmfound = True
+                if not dimmfound:
+                    meminfo = fishclient.sysinfo['MemorySummary']
+                    meminfo['Name'] = 'Memory'
+                    summary['badreadings'].append(SensorReading(meminfo))
+            for adapter in fishclient.sysinfo['PCIeDevices']:
+                adpinfo = fishclient._do_web_request(adapter['@odata.id'])
+                if adpinfo['Status']['Health'] not in ('OK', None):
+                    summary['badreadings'].append(SensorReading(adpinfo))
+            for fun in fishclient.sysinfo['PCIeFunctions']:
+                funinfo = fishclient._do_web_request(fun['@odata.id'])
+                if funinfo['Status']['Health'] not in ('OK', None):
+                    summary['badreadings'].append(SensorReading(funinfo))
+        if warnunknown and not summary['badreadings']:
+            unkinf = SensorReading({'Name': 'BMC',
+                                    'Status': {'Health': 'Unknown'}})
+            unkinf.states = ['System does not provide health information']
+            summary['badreadings'].append(unkinf)
+        return summary
+
     def user_delete(self, uid):
         # Redfish doesn't do so well with Deleting users either...
         # Blanking the username seems to be the convention
@@ -431,7 +529,7 @@
                 'UUID': self._varsysinfo.get('UUID', ''),
                 'Serial Number': self._varsysinfo.get('SerialNumber', ''),
                 'Manufacturer': self._varsysinfo.get('Manufacturer', ''),
-                'Product Name': self._varsysinfo.get('Model', ''),
+                'Product name': self._varsysinfo.get('Model', ''),
                 'Model': self._varsysinfo.get(
                     'SKU', self._varsysinfo.get('PartNumber', '')),
             }
@@ -446,7 +544,7 @@
             'UUID': self._varsysinfo.get('UUID', ''),
             'Serial Number': self._varsysinfo.get('SerialNumber', ''),
             'Manufacturer': self._varsysinfo.get('Manufacturer', ''),
-            'Product Name': self._varsysinfo.get('Model', ''),
+            'Product name': self._varsysinfo.get('Model', ''),
             'Model': self._varsysinfo.get(
                 'SKU', self._varsysinfo.get('PartNumber', '')),
         }
@@ -681,8 +779,78 @@
             'Remote media upload not supported on this platform')
 
     def update_firmware(self, filename, data=None, progress=None, bank=None):
-        raise exc.UnsupportedFunctionality(
-            'Firmware update not supported on this platform')
+        usd = self._do_web_request('/redfish/v1/UpdateService')
+        if usd.get('HttpPushUriTargetsBusy', False):
+            raise pygexc.TemporaryError('Cannot run multtiple updates to '
+                                        'same target concurrently')
+        try:
+            upurl = usd['HttpPushUri']
+        except KeyError:
+            raise pygexc.UnsupportedFunctionality('Redfish firmware update 
only supported for implementations with push update support')
+        if 'HttpPushUriTargetsBusy' in usd:
+            self._do_web_request(
+                '/redfish/v1/UpdateService',
+                {'HttpPushUriTargetsBusy': True}, method='PATCH')
+        try:
+            uploadthread = webclient.FileUploader(
+                self.webclient, upurl, filename, data, formwrap=False,
+                excepterror=False)
+            uploadthread.start()
+            wc = self.webclient
+            while uploadthread.isAlive():
+                uploadthread.join(3)
+                if progress:
+                    progress(
+                        {'phase': 'upload',
+                         'progress': 100 * wc.get_upload_progress()})
+            if (uploadthread.rspstatus >= 300
+                    or uploadthread.rspstatus < 200):
+                rsp = uploadthread.rsp
+                errmsg = ''
+                try:
+                    rsp = json.loads(rsp)
+                    errmsg = (
+                        rsp['error'][
+                            '@Message.ExtendedInfo'][0]['Message'])
+                except Exception:
+                    raise Exception(uploadthread.rsp)
+                raise Exception(errmsg)
+            rsp = json.loads(uploadthread.rsp)
+            monitorurl = rsp['@odata.id']
+            complete = False
+            phase = "apply"
+            statetype = 'TaskState'
+            while not complete:
+                pgress = self._do_web_request(monitorurl, cache=False)
+                if not pgress:
+                    break
+                for msg in pgress.get('Messages', []):
+                    if 'Verify failed' in msg.get('Message', ''):
+                        raise Exception(msg['Message'])
+                state = pgress[statetype]
+                if state in ('Cancelled', 'Exception', 'Interrupted',
+                             'Suspended'):
+                    raise Exception(
+                        json.dumps(json.dumps(pgress['Messages'])))
+                pct = float(pgress['PercentComplete'])
+                complete = state == 'Completed'
+                progress({'phase': phase, 'progress': pct})
+                if complete:
+                    if 'OperationTransitionedToJob' in 
pgress['Messages'][0]['MessageId']:
+                        monitorurl = pgress['Messages'][0]['MessageArgs'][0]
+                        phase = 'validating'
+                        statetype = 'JobState'
+                        complete = False
+                        time.sleep(3)
+                else:
+                    time.sleep(3)
+            return 'pending'
+        finally:
+            if 'HttpPushUriTargetsBusy' in usd:
+                self._do_web_request(
+                    '/redfish/v1/UpdateService',
+                    {'HttpPushUriTargetsBusy': False}, method='PATCH')
+
 
     def _do_bulk_requests(self, urls, cache=True):
         if self._gpool:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyghmi-1.5.61/pyghmi/redfish/oem/lenovo/xcc.py 
new/pyghmi-1.5.63/pyghmi/redfish/oem/lenovo/xcc.py
--- old/pyghmi-1.5.61/pyghmi/redfish/oem/lenovo/xcc.py  2023-04-13 
14:58:19.000000000 +0200
+++ new/pyghmi-1.5.63/pyghmi/redfish/oem/lenovo/xcc.py  2023-08-28 
16:39:39.000000000 +0200
@@ -24,7 +24,9 @@
 import time
 
 import six
+import zipfile
 
+import pyghmi.constants as pygconst
 import pyghmi.exceptions as pygexc
 import pyghmi.ipmi.private.util as util
 import pyghmi.ipmi.oem.lenovo.config as config
@@ -34,6 +36,21 @@
 from pyghmi.util.parse import parse_time
 import pyghmi.util.webclient as webclient
 
+class SensorReading(object):
+    def __init__(self, healthinfo, sensor=None, value=None, units=None,
+                 unavailable=False):
+        if sensor:
+            self.name = sensor['name']
+        else:
+            self.name = healthinfo['name']
+            self.health = healthinfo['health']
+            self.states = healthinfo['states']
+            self.state_ids = healthinfo.get('state_ids', None)
+        self.value = value
+        self.imprecision = None
+        self.units = units
+        self.unavailable = unavailable
+
 numregex = re.compile('([0-9]+)')
 funtypes = {
     0: 'RAID Controller',
@@ -236,6 +253,11 @@
                                 '({2})'.format(
                                     newvalue, key,
                                     ','.join(self.fwo[key]['possible'])))
+                elif self.fwo[key]['validexpression']:
+                    if not re.match(self.fwo[key]['validexpression'], 
newvalue):
+                        raise pygexc.InvalidParameterValue(
+                            '"{0}" does not match expression "{1}"'.format(
+                                newvalue, self.fwo[key]['validexpression']))
                 newnewvalues.append(newvalue)
             if len(newnewvalues) == 1:
                 self.fwo[key]['new_value'] = newnewvalues[0]
@@ -246,8 +268,11 @@
         if bay != -1:
             raise pygexc.UnsupportedFunctionality(
                 'This is not an enclosure manager')
-        rsp = self.wc.grab_json_response_with_status(
+        wc = self.wc.dupe(timeout=5)
+        rsp = wc.grab_json_response_with_status(
             '/api/providers/virt_reseat', '{}')
+        if rsp[1] == 500 and rsp[0] == 'Target Unavailable':
+            return
         if rsp[1] != 200 or rsp[0].get('return', 1) != 0:
             raise pygexc.UnsupportedFunctionality(
                 'This platform does not support AC reseat.')
@@ -477,6 +502,60 @@
                     self.wc.grab_json_response(
                         '/api/function', {'USB_AddMapping': newmapping})
 
+    def get_health(self, fishclient, verbose=True):
+        rsp = self._do_web_request('/api/providers/imm_active_events')
+        summary = {'badreadings': [], 'health': pygconst.Health.Ok}
+        fallbackdata = []
+        hmap = {
+            'I': pygconst.Health.Ok,
+            'E': pygconst.Health.Critical,
+            'W': pygconst.Health.Warning,
+        }
+        infoevents = False
+        existingevts = set([])
+        for item in rsp.get('items', ()):
+            # while usually the ipmi interrogation shall explain things,
+            # just in case there is a gap, make sure at least the
+            # health field is accurately updated
+            itemseverity = hmap.get(item.get('severity', 'E'),
+                                    pygconst.Health.Critical)
+            if itemseverity == pygconst.Health.Ok:
+                infoevents = True
+                continue
+            if (summary['health'] < itemseverity):
+                summary['health'] = itemseverity
+            if item['cmnid'] == 'FQXSPPW0104J':
+                # This event does not get modeled by the sensors
+                # add a made up sensor to explain
+                fallbackdata.append(
+                    SensorReading({'name': item['source'],
+                                       'states': ['Not Redundant'],
+                                       'state_ids': [3],
+                                       'health': pygconst.Health.Warning,
+                                       'type': 'Power'}, ''))
+            elif item['cmnid'] == 'FQXSFMA0041K':
+                fallbackdata.append(
+                    SensorReading({
+                        'name': 'Optane DCPDIMM',
+                        'health': pygconst.Health.Warning,
+                        'type': 'Memory',
+                        'states': [item['message']]},
+                        '')
+                )
+            else:
+                currevt = '{}:{}'.format(item['source'], item['message'])
+                if currevt in existingevts:
+                    continue
+                existingevts.add(currevt)
+                fallbackdata.append(SensorReading({
+                    'name': item['source'],
+                    'states': [item['message']],
+                    'health': itemseverity,
+                    'type': item['source'],
+                }, ''))
+        summary['badreadings'] = fallbackdata
+        return summary
+
     def get_description(self):
         description = self._do_web_request('/DeviceDescription.json')
         if description:
@@ -1121,6 +1200,29 @@
         if usd['HttpPushUriTargetsBusy']:
             raise pygexc.TemporaryError('Cannot run multtiple updates to '
                                         'same target concurrently')
+        z = None
+        wrappedfilename = None
+        uxzcount = 0
+        needseek = False
+        if data and hasattr(data, 'read'):
+            if zipfile.is_zipfile(data):
+                needseek = True
+                z = zipfile.ZipFile(data)
+            else:
+                data.seek(0)
+        elif data is None and zipfile.is_zipfile(filename):
+            z = zipfile.ZipFile(filename)
+        if z:
+            for tmpname in z.namelist():
+                if tmpname.startswith('payloads/'):
+                    uxzcount += 1
+                    if tmpname.endswith('.uxz'):
+                        wrappedfilename = tmpname
+        if uxzcount == 1 and wrappedfilename:
+            filename = os.path.basename(wrappedfilename)
+            data = z.open(wrappedfilename)
+        elif needseek:
+            data.seek(0)
         upurl = usd['HttpPushUri']
         self._do_web_request(
             '/redfish/v1/UpdateService',
@@ -1529,7 +1631,7 @@
                 'UUID': self._varsysinfo.get('UUID', ''),
                 'Serial Number': self._varsysinfo.get('SerialNumber', ''),
                 'Manufacturer': self._varsysinfo.get('Manufacturer', ''),
-                'Product Name': self._varsysinfo.get('Model', ''),
+                'Product name': self._varsysinfo.get('Model', ''),
                 'Model': self._varsysinfo.get(
                     'SKU', self._varsysinfo.get('PartNumber', '')),
             }
@@ -1546,7 +1648,7 @@
             'UUID': self._varsysinfo.get('UUID', ''),
             'Serial Number': self._varsysinfo.get('SerialNumber', ''),
             'Manufacturer': self._varsysinfo.get('Manufacturer', ''),
-            'Product Name': self._varsysinfo.get('Model', ''),
+            'Product name': self._varsysinfo.get('Model', ''),
             'Model': self._varsysinfo.get(
                 'SKU', self._varsysinfo.get('PartNumber', '')),
         }
@@ -1614,7 +1716,7 @@
                     if venid is not None:
                         bdata['PCI Vendor ID'] = '{0:04x}'.format(venid)
                     devid = fundata.get(self.ADP_DEVID, None)
-                    if devid is not None:
+                    if devid is not None and 'PCIE Device ID' not in bdata:
                         bdata['PCI Device ID'] = '{0:04x}'.format(devid)
                     venid = fundata.get(self.ADP_SUBVENID, None)
                     if venid is not None:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyghmi-1.5.61/pyghmi/util/webclient.py 
new/pyghmi-1.5.63/pyghmi/util/webclient.py
--- old/pyghmi-1.5.61/pyghmi/util/webclient.py  2023-04-13 14:58:19.000000000 
+0200
+++ new/pyghmi-1.5.63/pyghmi/util/webclient.py  2023-08-28 16:39:39.000000000 
+0200
@@ -160,9 +160,11 @@
             self.sock.close()
             self.sock = None
 
-    def dupe(self):
+    def dupe(self, timeout=None):
+        if timeout is None:
+            timeout = self.mytimeout
         return SecureHTTPConnection(self.thehost, self.theport, clone=self,
-                                    timeout=self.mytimeout)
+                                    timeout=timeout)
 
     def set_header(self, key, value):
         self.stdheaders[key] = value
@@ -317,11 +319,19 @@
             ulhdrs['Content-Length'] = len(uploadforms[filename])
             self.ulsize = len(uploadforms[filename])
         else:
-            curroff = data.tell()
-            data.seek(0, 2)
-            self.ulsize = data.tell()
-            data.seek(curroff, 0)
-            self._upbuffer = data
+            canseek = True
+            try:
+                curroff = data.tell()
+            except io.UnsupportedOperation:
+                canseek = False
+                databytes = data.read()
+                self.ulsize = len(databytes)
+                self._upbuffer = io.BytesIO(databytes)
+            if canseek:
+                data.seek(0, 2)
+                self.ulsize = data.tell()
+                data.seek(curroff, 0)
+                self._upbuffer = data
             ulhdrs['Content-Type'] = b'application/octet-stream'
             ulhdrs['Content-Length'] = self.ulsize
         webclient = self.dupe()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyghmi-1.5.61/pyghmi.egg-info/PKG-INFO 
new/pyghmi-1.5.63/pyghmi.egg-info/PKG-INFO
--- old/pyghmi-1.5.61/pyghmi.egg-info/PKG-INFO  2023-04-13 14:58:48.000000000 
+0200
+++ new/pyghmi-1.5.63/pyghmi.egg-info/PKG-INFO  2023-08-28 16:40:08.000000000 
+0200
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: pyghmi
-Version: 1.5.61
+Version: 1.5.63
 Summary: Python General Hardware Management Initiative (IPMI and others)
 Home-page: http://github.com/openstack/pyghmi/
 Author: Jarrod Johnson
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyghmi-1.5.61/pyghmi.egg-info/pbr.json 
new/pyghmi-1.5.63/pyghmi.egg-info/pbr.json
--- old/pyghmi-1.5.61/pyghmi.egg-info/pbr.json  2023-04-13 14:58:48.000000000 
+0200
+++ new/pyghmi-1.5.63/pyghmi.egg-info/pbr.json  2023-08-28 16:40:08.000000000 
+0200
@@ -1 +1 @@
-{"git_version": "537fccb", "is_release": true}
\ No newline at end of file
+{"git_version": "b2f46df", "is_release": true}
\ No newline at end of file

Reply via email to