pyghmi 1.5.72__py3-none-any.whl → 1.5.76__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyghmi/ipmi/command.py +6 -2
- pyghmi/ipmi/oem/generic.py +3 -1
- pyghmi/ipmi/oem/lenovo/handler.py +3 -1
- pyghmi/ipmi/oem/lenovo/imm.py +9 -0
- pyghmi/redfish/command.py +225 -146
- pyghmi/redfish/oem/generic.py +274 -85
- pyghmi/redfish/oem/lenovo/main.py +10 -3
- pyghmi/redfish/oem/lenovo/smm3.py +85 -0
- pyghmi/redfish/oem/lenovo/xcc.py +1 -0
- pyghmi/redfish/oem/lenovo/xcc3.py +324 -13
- pyghmi/util/webclient.py +32 -8
- {pyghmi-1.5.72.dist-info → pyghmi-1.5.76.dist-info}/METADATA +6 -7
- {pyghmi-1.5.72.dist-info → pyghmi-1.5.76.dist-info}/RECORD +19 -18
- {pyghmi-1.5.72.dist-info → pyghmi-1.5.76.dist-info}/WHEEL +1 -1
- {pyghmi-1.5.72.dist-info → pyghmi-1.5.76.dist-info}/entry_points.txt +0 -1
- pyghmi-1.5.76.dist-info/pbr.json +1 -0
- pyghmi-1.5.72.dist-info/pbr.json +0 -1
- {pyghmi-1.5.72.dist-info → pyghmi-1.5.76.dist-info}/AUTHORS +0 -0
- {pyghmi-1.5.72.dist-info → pyghmi-1.5.76.dist-info}/LICENSE +0 -0
- {pyghmi-1.5.72.dist-info → pyghmi-1.5.76.dist-info}/top_level.txt +0 -0
pyghmi/redfish/oem/generic.py
CHANGED
@@ -17,12 +17,17 @@ from fnmatch import fnmatch
|
|
17
17
|
import json
|
18
18
|
import os
|
19
19
|
import re
|
20
|
+
from datetime import datetime
|
21
|
+
from datetime import timedelta
|
22
|
+
from dateutil import tz
|
20
23
|
import time
|
21
24
|
|
22
25
|
import pyghmi.constants as const
|
23
26
|
import pyghmi.exceptions as exc
|
24
27
|
import pyghmi.media as media
|
25
28
|
import pyghmi.util.webclient as webclient
|
29
|
+
from pyghmi.util.parse import parse_time
|
30
|
+
|
26
31
|
|
27
32
|
|
28
33
|
class SensorReading(object):
|
@@ -179,6 +184,7 @@ class AttrDependencyHandler(object):
|
|
179
184
|
|
180
185
|
class OEMHandler(object):
|
181
186
|
hostnic = None
|
187
|
+
usegenericsensors = True
|
182
188
|
|
183
189
|
def __init__(self, sysinfo, sysurl, webclient, cache, gpool=None):
|
184
190
|
self._gpool = gpool
|
@@ -187,6 +193,122 @@ class OEMHandler(object):
|
|
187
193
|
self._urlcache = cache
|
188
194
|
self.webclient = webclient
|
189
195
|
|
196
|
+
def supports_expand(self, url):
|
197
|
+
# Unfortunately, the state of expand in redfish is pretty dicey,
|
198
|
+
# so an OEM handler must opt into this behavior
|
199
|
+
# There is a way an implementation advertises support, however
|
200
|
+
# this isn't to be trusted.
|
201
|
+
# Even among some generally reputable implementations, they will fail in some scenarios
|
202
|
+
# and you'll see in their documentation "some urls will fail if you try to expand them"
|
203
|
+
# perhaps being specific, but other times being vague, but in either case,
|
204
|
+
# nothing programattic to consume to know when to do or not do an expand..
|
205
|
+
return False
|
206
|
+
|
207
|
+
def get_system_power_watts(self, fishclient):
|
208
|
+
totalwatts = 0
|
209
|
+
gotpower = False
|
210
|
+
for chassis in fishclient.sysinfo.get('Links', {}).get('Chassis', []):
|
211
|
+
envinfo = fishclient._get_chassis_env(chassis)
|
212
|
+
currwatts = envinfo.get('watts', None)
|
213
|
+
if currwatts is not None:
|
214
|
+
gotpower = True
|
215
|
+
totalwatts += envinfo['watts']
|
216
|
+
if not gotpower:
|
217
|
+
raise exc.UnsupportedFunctionality("System does not provide Power under redfish EnvironmentMetrics")
|
218
|
+
return totalwatts
|
219
|
+
|
220
|
+
def _get_cpu_temps(self, fishclient):
|
221
|
+
cputemps = []
|
222
|
+
for chassis in fishclient.sysinfo.get('Links', {}).get('Chassis', []):
|
223
|
+
thermals = fishclient._get_thermals(chassis)
|
224
|
+
for temp in thermals:
|
225
|
+
if temp.get('PhysicalContext', '') != 'CPU':
|
226
|
+
continue
|
227
|
+
if temp.get('ReadingCelsius', None) is None:
|
228
|
+
continue
|
229
|
+
cputemps.append(temp)
|
230
|
+
return cputemps
|
231
|
+
|
232
|
+
def get_event_log(self, clear=False, fishclient=None, extraurls=[]):
|
233
|
+
bmcinfo = self._do_web_request(fishclient._bmcurl)
|
234
|
+
lsurl = bmcinfo.get('LogServices', {}).get('@odata.id', None)
|
235
|
+
if not lsurl:
|
236
|
+
return
|
237
|
+
currtime = bmcinfo.get('DateTime', None)
|
238
|
+
correction = timedelta(0)
|
239
|
+
utz = tz.tzoffset('', 0)
|
240
|
+
ltz = tz.gettz()
|
241
|
+
if currtime:
|
242
|
+
currtime = parse_time(currtime)
|
243
|
+
if currtime:
|
244
|
+
now = datetime.now(utz)
|
245
|
+
try:
|
246
|
+
correction = now - currtime
|
247
|
+
except TypeError:
|
248
|
+
correction = now - currtime.replace(tzinfo=utz)
|
249
|
+
lurls = self._do_web_request(lsurl).get('Members', [])
|
250
|
+
lurls.extend(extraurls)
|
251
|
+
for lurl in lurls:
|
252
|
+
lurl = lurl['@odata.id']
|
253
|
+
loginfo = self._do_web_request(lurl, cache=(not clear))
|
254
|
+
entriesurl = loginfo.get('Entries', {}).get('@odata.id', None)
|
255
|
+
if not entriesurl:
|
256
|
+
continue
|
257
|
+
logid = loginfo.get('Id', '')
|
258
|
+
entries = self._do_web_request(entriesurl, cache=False)
|
259
|
+
if clear:
|
260
|
+
# The clear is against the log service etag, not entries
|
261
|
+
# so we have to fetch service etag after we fetch entries
|
262
|
+
# until we can verify that the etag is consistent to prove
|
263
|
+
# that the clear is atomic
|
264
|
+
newloginfo = self._do_web_request(lurl, cache=False)
|
265
|
+
clearurl = newloginfo.get('Actions', {}).get(
|
266
|
+
'#LogService.ClearLog', {}).get('target', '')
|
267
|
+
while clearurl:
|
268
|
+
try:
|
269
|
+
self._do_web_request(clearurl, method='POST',
|
270
|
+
payload={})
|
271
|
+
clearurl = False
|
272
|
+
except exc.PyghmiException as e:
|
273
|
+
if 'EtagPreconditionalFailed' not in str(e):
|
274
|
+
raise
|
275
|
+
# This doesn't guarantee atomicity, but it mitigates
|
276
|
+
# greatly. Unfortunately some implementations
|
277
|
+
# mutate the tag endlessly and we have no hope
|
278
|
+
entries = self._do_web_request(entriesurl, cache=False)
|
279
|
+
newloginfo = self._do_web_request(lurl, cache=False)
|
280
|
+
for log in entries.get('Members', []):
|
281
|
+
if ('Created' not in log and 'Message' not in log
|
282
|
+
and 'Severity' not in log):
|
283
|
+
# without any data, this log entry isn't actionable
|
284
|
+
continue
|
285
|
+
record = {}
|
286
|
+
record['log_id'] = logid
|
287
|
+
parsedtime = parse_time(log.get('Created', ''))
|
288
|
+
if not parsedtime:
|
289
|
+
parsedtime = parse_time(log.get('EventTimestamp', ''))
|
290
|
+
if parsedtime:
|
291
|
+
entime = parsedtime + correction
|
292
|
+
entime = entime.astimezone(ltz)
|
293
|
+
record['timestamp'] = entime.strftime('%Y-%m-%dT%H:%M:%S')
|
294
|
+
else:
|
295
|
+
record['timestamp'] = log.get('Created', '')
|
296
|
+
record['message'] = log.get('Message', None)
|
297
|
+
record['severity'] = _healthmap.get(
|
298
|
+
log.get('Severity', 'Warning'), const.Health.Ok)
|
299
|
+
yield record
|
300
|
+
|
301
|
+
def get_average_processor_temperature(self, fishclient):
|
302
|
+
cputemps = self._get_cpu_temps(fishclient)
|
303
|
+
if not cputemps:
|
304
|
+
return SensorReading(
|
305
|
+
None, {'name': 'Average Processor Temperature'}, value=None, units='°C',
|
306
|
+
unavailable=True)
|
307
|
+
cputemps = [x['ReadingCelsius'] for x in cputemps]
|
308
|
+
avgtemp = sum(cputemps) / len(cputemps)
|
309
|
+
return SensorReading(
|
310
|
+
None, {'name': 'Average Processor Temperature'}, value=avgtemp, units='°C')
|
311
|
+
|
190
312
|
def get_health(self, fishclient, verbose=True):
|
191
313
|
health = fishclient.sysinfo.get('Status', {})
|
192
314
|
health = health.get('HealthRollup', health.get('Health', 'Unknown'))
|
@@ -226,9 +348,8 @@ class OEMHandler(object):
|
|
226
348
|
memsumstatus.get('Health', None))
|
227
349
|
if memsumstatus != 'OK':
|
228
350
|
dimmfound = False
|
229
|
-
|
230
|
-
|
231
|
-
dimminfo = fishclient._do_web_request(mem['@odata.id'])
|
351
|
+
dimmdata = self._get_mem_data()
|
352
|
+
for dimminfo in dimmdata['Members']:
|
232
353
|
if dimminfo.get('Status', {}).get(
|
233
354
|
'State', None) == 'Absent':
|
234
355
|
continue
|
@@ -520,8 +641,15 @@ class OEMHandler(object):
|
|
520
641
|
return None
|
521
642
|
|
522
643
|
def get_description(self, fishclient):
|
644
|
+
for chassis in fishclient.sysinfo.get('Links', {}).get('Chassis', []):
|
645
|
+
chassisurl = chassis['@odata.id']
|
646
|
+
chassisinfo = self._do_web_request(chassisurl)
|
647
|
+
hmm = chassisinfo.get('HeightMm', None)
|
648
|
+
if hmm:
|
649
|
+
return {'height': hmm/44.45}
|
523
650
|
return {}
|
524
651
|
|
652
|
+
|
525
653
|
def get_firmware_inventory(self, components, fishclient):
|
526
654
|
return []
|
527
655
|
|
@@ -561,15 +689,30 @@ class OEMHandler(object):
|
|
561
689
|
for adp in self._get_adp_inventory(True, withids):
|
562
690
|
yield adp
|
563
691
|
|
692
|
+
def _get_node_info(self):
|
693
|
+
nodeinfo = self._varsysinfo
|
694
|
+
if not nodeinfo:
|
695
|
+
overview = self._do_web_request('/redfish/v1/')
|
696
|
+
chassismembs = overview.get('Chassis', {}).get('@odata.id', None)
|
697
|
+
if not chassismembs:
|
698
|
+
return nodeinfo
|
699
|
+
chassislist = self._do_web_request(chassismembs)
|
700
|
+
chassismembs = chassislist.get('Members', [])
|
701
|
+
if len(chassismembs) == 1:
|
702
|
+
chassisurl = chassismembs[0]['@odata.id']
|
703
|
+
nodeinfo = self._do_web_request(chassisurl)
|
704
|
+
return nodeinfo
|
705
|
+
|
564
706
|
def get_inventory_of_component(self, component):
|
565
707
|
if component.lower() == 'system':
|
708
|
+
nodeinfo = self._get_node_info()
|
566
709
|
sysinfo = {
|
567
|
-
'UUID':
|
568
|
-
'Serial Number':
|
569
|
-
'Manufacturer':
|
570
|
-
'Product name':
|
571
|
-
'Model':
|
572
|
-
'SKU',
|
710
|
+
'UUID': nodeinfo.get('UUID', ''),
|
711
|
+
'Serial Number': nodeinfo.get('SerialNumber', ''),
|
712
|
+
'Manufacturer': nodeinfo.get('Manufacturer', ''),
|
713
|
+
'Product name': nodeinfo.get('Model', ''),
|
714
|
+
'Model': nodeinfo.get(
|
715
|
+
'SKU', nodeinfo.get('PartNumber', '')),
|
573
716
|
}
|
574
717
|
if sysinfo['UUID'] and '-' not in sysinfo['UUID']:
|
575
718
|
sysinfo['UUID'] = '-'.join((
|
@@ -584,13 +727,14 @@ class OEMHandler(object):
|
|
584
727
|
return invpair[1]
|
585
728
|
|
586
729
|
def get_inventory(self, withids=False):
|
730
|
+
nodeinfo = self._get_node_info()
|
587
731
|
sysinfo = {
|
588
|
-
'UUID':
|
589
|
-
'Serial Number':
|
590
|
-
'Manufacturer':
|
591
|
-
'Product name':
|
592
|
-
'Model':
|
593
|
-
'SKU',
|
732
|
+
'UUID': nodeinfo.get('UUID', ''),
|
733
|
+
'Serial Number': nodeinfo.get('SerialNumber', ''),
|
734
|
+
'Manufacturer': nodeinfo.get('Manufacturer', ''),
|
735
|
+
'Product name': nodeinfo.get('Model', ''),
|
736
|
+
'Model': nodeinfo.get(
|
737
|
+
'SKU', nodeinfo.get('PartNumber', '')),
|
594
738
|
}
|
595
739
|
if sysinfo['UUID'] and '-' not in sysinfo['UUID']:
|
596
740
|
sysinfo['UUID'] = '-'.join((
|
@@ -600,32 +744,14 @@ class OEMHandler(object):
|
|
600
744
|
sysinfo['UUID'] = sysinfo['UUID'].lower()
|
601
745
|
yield ('System', sysinfo)
|
602
746
|
self._hwnamemap = {}
|
603
|
-
cpumemurls = []
|
604
|
-
memurl = self._varsysinfo.get('Memory', {}).get('@odata.id', None)
|
605
|
-
if memurl:
|
606
|
-
cpumemurls.append(memurl)
|
607
|
-
cpurl = self._varsysinfo.get('Processors', {}).get('@odata.id', None)
|
608
|
-
if cpurl:
|
609
|
-
cpumemurls.append(cpurl)
|
610
|
-
list(self._do_bulk_requests(cpumemurls))
|
611
747
|
adpurls = self._get_adp_urls()
|
612
|
-
if cpurl:
|
613
|
-
cpurls = self._get_cpu_urls()
|
614
|
-
else:
|
615
|
-
cpurls = []
|
616
|
-
if memurl:
|
617
|
-
memurls = self._get_mem_urls()
|
618
|
-
else:
|
619
|
-
memurls = []
|
620
748
|
diskurls = self._get_disk_urls()
|
621
|
-
allurls = adpurls +
|
749
|
+
allurls = adpurls + diskurls
|
622
750
|
list(self._do_bulk_requests(allurls))
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
for mem in self._get_mem_inventory(withids=withids, urls=memurls):
|
628
|
-
yield mem
|
751
|
+
for cpu in self._get_cpu_inventory(withids=withids):
|
752
|
+
yield cpu
|
753
|
+
for mem in self._get_mem_inventory(withids=withids):
|
754
|
+
yield mem
|
629
755
|
for adp in self._get_adp_inventory(withids=withids, urls=adpurls):
|
630
756
|
yield adp
|
631
757
|
for disk in self._get_disk_inventory(withids=withids, urls=diskurls):
|
@@ -652,34 +778,55 @@ class OEMHandler(object):
|
|
652
778
|
foundmacs = False
|
653
779
|
macinfobyadpname = {}
|
654
780
|
if 'NetworkInterfaces' in self._varsysinfo:
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
nadurl = nifinfo.get('Links', {}).get('NetworkAdapter', {}).get("@odata.id")
|
781
|
+
nifdata = self._get_expanded_data(
|
782
|
+
self._varsysinfo['NetworkInterfaces']['@odata.id'])
|
783
|
+
for nifinfo in nifdata.get('Members', []):
|
784
|
+
nadurl = nifinfo.get(
|
785
|
+
'Links', {}).get('NetworkAdapter', {}).get("@odata.id")
|
662
786
|
if nadurl:
|
663
787
|
nadinfo = self._do_web_request(nadurl)
|
664
788
|
if 'Name' not in nadinfo:
|
665
789
|
continue
|
666
790
|
nicname = nadinfo['Name']
|
791
|
+
if nicname == 'NetworkAdapter':
|
792
|
+
nicname = nadinfo.get('Model', nicname)
|
667
793
|
yieldinf = {}
|
668
794
|
macidx = 1
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
'
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
795
|
+
if 'Ports' in nadinfo:
|
796
|
+
for portinfo in self._get_expanded_data(
|
797
|
+
nadinfo['Ports']['@odata.id']).get('Members', []):
|
798
|
+
ethinfo = portinfo.get('Ethernet', {})
|
799
|
+
if ethinfo:
|
800
|
+
macs = [x for x in ethinfo.get('AssociatedMACAddresses', [])]
|
801
|
+
for mac in macs:
|
802
|
+
label = 'MAC Address {}'.format(macidx)
|
803
|
+
yieldinf[label] = _normalize_mac(mac)
|
804
|
+
macidx += 1
|
805
|
+
foundmacs = True
|
806
|
+
ibinfo = portinfo.get('InfiniBand', {})
|
807
|
+
if ibinfo:
|
808
|
+
macs = [x for x in ibinfo.get('AssociatedPortGUIDs', [])]
|
809
|
+
for mac in macs:
|
810
|
+
label = 'Port GUID {}'.format(macidx)
|
811
|
+
yieldinf[label] = mac
|
812
|
+
macidx += 1
|
813
|
+
foundmacs = True
|
814
|
+
macinfobyadpname[nicname] = yieldinf
|
815
|
+
else:
|
816
|
+
for ctrlr in nadinfo.get('Controllers', []):
|
817
|
+
porturls = [x['@odata.id'] for x in ctrlr.get(
|
818
|
+
'Links', {}).get('Ports', [])]
|
819
|
+
for porturl in porturls:
|
820
|
+
portinfo = self._do_web_request(porturl)
|
821
|
+
macs = [x for x in portinfo.get(
|
822
|
+
'Ethernet', {}).get(
|
823
|
+
'AssociatedMACAddresses', [])]
|
824
|
+
for mac in macs:
|
825
|
+
label = 'MAC Address {}'.format(macidx)
|
826
|
+
yieldinf[label] = _normalize_mac(mac)
|
827
|
+
macidx += 1
|
828
|
+
foundmacs = True
|
829
|
+
macinfobyadpname[nicname] = yieldinf
|
683
830
|
if not urls:
|
684
831
|
urls = self._get_adp_urls()
|
685
832
|
for inf in self._do_bulk_requests(urls):
|
@@ -730,6 +877,8 @@ class OEMHandler(object):
|
|
730
877
|
if aname in macinfobyadpname:
|
731
878
|
del macinfobyadpname[aname]
|
732
879
|
yield aname, yieldinf
|
880
|
+
if onlyname:
|
881
|
+
return
|
733
882
|
if macinfobyadpname:
|
734
883
|
for adp in macinfobyadpname:
|
735
884
|
yield adp, macinfobyadpname[adp]
|
@@ -774,12 +923,9 @@ class OEMHandler(object):
|
|
774
923
|
return urls
|
775
924
|
|
776
925
|
def _get_cpu_inventory(self, onlynames=False, withids=False, urls=None):
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
return
|
781
|
-
for res in self._do_bulk_requests(urls):
|
782
|
-
currcpuinfo, url = res
|
926
|
+
for currcpuinfo in self._get_cpu_data().get(
|
927
|
+
'Members', []):
|
928
|
+
url = currcpuinfo['@odata.id']
|
783
929
|
name = currcpuinfo.get('Name', 'CPU')
|
784
930
|
if name in self._hwnamemap:
|
785
931
|
self._hwnamemap[name] = None
|
@@ -804,21 +950,20 @@ class OEMHandler(object):
|
|
804
950
|
return urls
|
805
951
|
|
806
952
|
def _get_cpu_urls(self):
|
953
|
+
md = self._get_cpu_data(False)
|
954
|
+
return [x['@odata.id'] for x in md.get('Members', [])]
|
955
|
+
|
956
|
+
def _get_cpu_data(self, expand='.'):
|
807
957
|
cpurl = self._varsysinfo.get('Processors', {}).get('@odata.id', None)
|
808
|
-
if cpurl
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
urls = [x['@odata.id'] for x in cpurl.get('Members', [])]
|
813
|
-
return urls
|
958
|
+
if not cpurl:
|
959
|
+
return {}
|
960
|
+
return self._get_expanded_data(cpurl, expand)
|
961
|
+
|
814
962
|
|
815
963
|
def _get_mem_inventory(self, onlyname=False, withids=False, urls=None):
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
return
|
820
|
-
for mem in self._do_bulk_requests(urls):
|
821
|
-
currmeminfo, url = mem
|
964
|
+
memdata = self._get_mem_data()
|
965
|
+
for currmeminfo in memdata.get('Members', []): # self._do_bulk_requests(urls):
|
966
|
+
url = currmeminfo['@odata.id']
|
822
967
|
name = currmeminfo.get('Name', 'Memory')
|
823
968
|
if name in self._hwnamemap:
|
824
969
|
self._hwnamemap[name] = None
|
@@ -847,13 +992,31 @@ class OEMHandler(object):
|
|
847
992
|
yield (name, meminfo)
|
848
993
|
|
849
994
|
def _get_mem_urls(self):
|
995
|
+
md = self._get_mem_data(False)
|
996
|
+
return [x['@odata.id'] for x in md.get('Members', [])]
|
997
|
+
|
998
|
+
def _get_mem_data(self, expand='.'):
|
850
999
|
memurl = self._varsysinfo.get('Memory', {}).get('@odata.id', None)
|
851
1000
|
if not memurl:
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
|
856
|
-
|
1001
|
+
return {}
|
1002
|
+
return self._get_expanded_data(memurl, expand)
|
1003
|
+
|
1004
|
+
def _get_expanded_data(self, url, expand='.'):
|
1005
|
+
topdata = []
|
1006
|
+
if not url:
|
1007
|
+
return topdata
|
1008
|
+
if not expand:
|
1009
|
+
return self._do_web_request(url)
|
1010
|
+
elif self.supports_expand(url):
|
1011
|
+
return self._do_web_request(url + '?$expand=' + expand)
|
1012
|
+
else: # emulate expand behavior
|
1013
|
+
topdata = self._do_web_request(url)
|
1014
|
+
newmembers = []
|
1015
|
+
for x in topdata.get('Members', []):
|
1016
|
+
newmembers.append(self._do_web_request(x['@odata.id']))
|
1017
|
+
topdata['Members'] = newmembers
|
1018
|
+
return topdata
|
1019
|
+
return topdata
|
857
1020
|
|
858
1021
|
def get_storage_configuration(self):
|
859
1022
|
raise exc.UnsupportedFunctionality(
|
@@ -875,8 +1038,9 @@ class OEMHandler(object):
|
|
875
1038
|
raise exc.UnsupportedFunctionality(
|
876
1039
|
'Remote media upload not supported on this platform')
|
877
1040
|
|
878
|
-
def update_firmware(self, filename, data=None, progress=None, bank=None):
|
879
|
-
|
1041
|
+
def update_firmware(self, filename, data=None, progress=None, bank=None, otherfields=()):
|
1042
|
+
# disable cache to make sure we trigger the token renewal logic if needed
|
1043
|
+
usd = self._do_web_request('/redfish/v1/UpdateService', cache=False)
|
880
1044
|
upurl = usd.get('MultipartHttpPushUri', None)
|
881
1045
|
ismultipart = True
|
882
1046
|
if not upurl:
|
@@ -895,7 +1059,7 @@ class OEMHandler(object):
|
|
895
1059
|
try:
|
896
1060
|
uploadthread = webclient.FileUploader(
|
897
1061
|
self.webclient, upurl, filename, data, formwrap=ismultipart,
|
898
|
-
excepterror=False)
|
1062
|
+
excepterror=False, otherfields=otherfields)
|
899
1063
|
uploadthread.start()
|
900
1064
|
wc = self.webclient
|
901
1065
|
while uploadthread.isAlive():
|
@@ -976,6 +1140,26 @@ class OEMHandler(object):
|
|
976
1140
|
cache=True):
|
977
1141
|
return self._do_web_request(url, payload, method, cache), url
|
978
1142
|
|
1143
|
+
def _get_session_token(self, wc):
|
1144
|
+
username = self.username
|
1145
|
+
password = self.password
|
1146
|
+
if not isinstance(username, str):
|
1147
|
+
username = username.decode()
|
1148
|
+
if not isinstance(password, str):
|
1149
|
+
password = password.decode()
|
1150
|
+
# specification actually indicates we can skip straight to this url
|
1151
|
+
rsp = wc.grab_rsp('/redfish/v1/SessionService/Sessions',
|
1152
|
+
{'UserName': username, 'Password': password})
|
1153
|
+
rsp.read()
|
1154
|
+
self.xauthtoken = rsp.getheader('X-Auth-Token')
|
1155
|
+
if self.xauthtoken:
|
1156
|
+
if 'Authorization' in wc.stdheaders:
|
1157
|
+
del wc.stdheaders['Authorization']
|
1158
|
+
if 'Authorization' in self.webclient.stdheaders:
|
1159
|
+
del self.webclient.stdheaders['Authorization']
|
1160
|
+
wc.stdheaders['X-Auth-Token'] = self.xauthtoken
|
1161
|
+
self.webclient.stdheaders['X-Auth-Token'] = self.xauthtoken
|
1162
|
+
|
979
1163
|
def _do_web_request(self, url, payload=None, method=None, cache=True):
|
980
1164
|
res = None
|
981
1165
|
if cache and payload is None and method is None:
|
@@ -984,6 +1168,11 @@ class OEMHandler(object):
|
|
984
1168
|
return res
|
985
1169
|
wc = self.webclient.dupe()
|
986
1170
|
res = wc.grab_json_response_with_status(url, payload, method=method)
|
1171
|
+
if res[1] == 401 and 'X-Auth-Token' in self.webclient.stdheaders:
|
1172
|
+
wc.set_basic_credentials(self.username, self.password)
|
1173
|
+
self._get_session_token(wc)
|
1174
|
+
res = wc.grab_json_response_with_status(url, payload,
|
1175
|
+
method=method)
|
987
1176
|
if res[1] < 200 or res[1] >= 300:
|
988
1177
|
try:
|
989
1178
|
info = json.loads(res[0])
|
@@ -16,7 +16,7 @@ import pyghmi.redfish.oem.generic as generic
|
|
16
16
|
from pyghmi.redfish.oem.lenovo import tsma
|
17
17
|
from pyghmi.redfish.oem.lenovo import xcc
|
18
18
|
from pyghmi.redfish.oem.lenovo import xcc3
|
19
|
-
|
19
|
+
from pyghmi.redfish.oem.lenovo import smm3
|
20
20
|
|
21
21
|
def get_handler(sysinfo, sysurl, webclient, cache, cmd):
|
22
22
|
leninf = sysinfo.get('Oem', {}).get('Lenovo', {})
|
@@ -45,5 +45,12 @@ def get_handler(sysinfo, sysurl, webclient, cache, cmd):
|
|
45
45
|
if 'hdd' in leninv and 'hostMAC' in leninv and 'backPlane' in leninv:
|
46
46
|
return tsma.TsmHandler(sysinfo, sysurl, webclient, cache,
|
47
47
|
gpool=cmd._gpool)
|
48
|
-
|
49
|
-
|
48
|
+
try:
|
49
|
+
devdesc = webclient.grab_json_response_with_status('/DeviceDescription.json')
|
50
|
+
if devdesc[1] == 200:
|
51
|
+
if devdesc[0]['type'].lower() == 'lenovo-smm3':
|
52
|
+
return smm3.OEMHandler(sysinfo, sysurl, webclient, cache, gpool=cmd._gpool)
|
53
|
+
except Exception:
|
54
|
+
pass
|
55
|
+
return generic.OEMHandler(sysinfo, sysurl, webclient, cache,
|
56
|
+
gpool=cmd._gpool)
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# Copyright 2025 Lenovo Corporation
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
import pyghmi.redfish.oem.generic as generic
|
16
|
+
import pyghmi.constants as pygconst
|
17
|
+
|
18
|
+
healthlookup = {
|
19
|
+
'ok': pygconst.Health.Ok,
|
20
|
+
'critical': pygconst.Health.Critical
|
21
|
+
}
|
22
|
+
|
23
|
+
def _baytonumber(bay):
|
24
|
+
try:
|
25
|
+
return int(bay)
|
26
|
+
except ValueError:
|
27
|
+
if len(bay) == 2:
|
28
|
+
# Treat a hexadecimal system as a leading decimal digit and letter compile
|
29
|
+
# 1a == slot 1, 1b == slot 2, 2a == slot 1, etc..
|
30
|
+
try:
|
31
|
+
tmp = int(bay, 16)
|
32
|
+
return (2 * (tmp >> 4) - 1) + ((tmp & 15) % 10)
|
33
|
+
except ValueError:
|
34
|
+
return None
|
35
|
+
return None
|
36
|
+
|
37
|
+
|
38
|
+
def _baytolabel(bay):
|
39
|
+
try:
|
40
|
+
baynum = int(bay)
|
41
|
+
# need to convert to 1a, 1b, etc...
|
42
|
+
vertidx = ((baynum - 1) // 2 + 1) << 4
|
43
|
+
horizidx = (baynum - 1) % 2 + 10
|
44
|
+
bayid = vertidx | horizidx
|
45
|
+
return '{:02x}'.format(bayid)
|
46
|
+
except ValueError:
|
47
|
+
return bay
|
48
|
+
return None
|
49
|
+
|
50
|
+
class OEMHandler(generic.OEMHandler):
|
51
|
+
def get_health(self, fishclient, verbose=True):
|
52
|
+
rsp = self._do_web_request('/redfish/v1/Chassis/chassis1')
|
53
|
+
health = rsp.get('Status', {}).get('Health', 'Unknown').lower()
|
54
|
+
health = healthlookup.get(health, pygconst.Health.Critical)
|
55
|
+
return {'health': health}
|
56
|
+
|
57
|
+
def get_system_configuration(self, hideadvanced=True, fishclient=None):
|
58
|
+
return {}
|
59
|
+
|
60
|
+
def _get_node_info(self):
|
61
|
+
nodeinfo = self._varsysinfo
|
62
|
+
if not nodeinfo:
|
63
|
+
overview = self._do_web_request('/redfish/v1/')
|
64
|
+
chassismembs = overview.get('Chassis', {}).get('@odata.id', None)
|
65
|
+
if not chassismembs:
|
66
|
+
return nodeinfo
|
67
|
+
chassislist = self._do_web_request(chassismembs)
|
68
|
+
chassismembs = chassislist.get('Members', [])
|
69
|
+
if len(chassismembs) == 1:
|
70
|
+
chassisurl = chassismembs[0]['@odata.id']
|
71
|
+
nodeinfo = self._do_web_request(chassisurl)
|
72
|
+
nodeinfo['SKU'] = nodeinfo['Model']
|
73
|
+
nodeinfo['Model'] = 'N1380 Enclosure'
|
74
|
+
return nodeinfo
|
75
|
+
|
76
|
+
def reseat_bay(self, bay):
|
77
|
+
bayid = _baytolabel(bay)
|
78
|
+
url = '/redfish/v1/Chassis/chassis1/Oem/Lenovo/Nodes/{}/Actions/Node.Reseat'.format(bayid)
|
79
|
+
rsp = self._do_web_request(url, method='POST')
|
80
|
+
|
81
|
+
def get_event_log(self, clear=False, fishclient=None):
|
82
|
+
return super().get_event_log(clear, fishclient, extraurls=[{'@odata.id':'/redfish/v1/Chassis/chassis1/LogServices/EventLog'}])
|
83
|
+
|
84
|
+
def get_description(self, fishclient):
|
85
|
+
return {'height': 13, 'slot': 0, 'slots': [8, 2]}
|
pyghmi/redfish/oem/lenovo/xcc.py
CHANGED