pyghmi 1.6.1__py3-none-any.whl → 1.6.3__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.
@@ -375,6 +375,10 @@ class Session(object):
375
375
  tmpsocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
376
376
  else:
377
377
  tmpsocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
378
+ try:
379
+ tmpsocket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 16777216)
380
+ except Exception:
381
+ pass
378
382
  if server is None:
379
383
  # Rather than wait until send() to bind, bind now so that we have
380
384
  # a port number allocated no matter what
@@ -193,6 +193,7 @@ class OEMHandler(object):
193
193
  self._varsysurl = sysurl
194
194
  self._urlcache = cache
195
195
  self.webclient = webclient
196
+ self._hwnamemap = {}
196
197
 
197
198
  def get_screenshot(self, outfile):
198
199
  raise exc.UnsupportedFunctionality(
@@ -13,14 +13,39 @@
13
13
  # limitations under the License.
14
14
  import copy
15
15
  import json
16
+ import re
16
17
  import pyghmi.constants as pygconst
17
18
  import pyghmi.redfish.oem.generic as generic
18
19
  import pyghmi.exceptions as pygexc
19
20
  import pyghmi.util.webclient as webclient
21
+ import pyghmi.storage as storage
22
+ import pyghmi.ipmi.private.util as util
20
23
  import zipfile
21
24
  import time
22
25
  import os.path
23
26
 
27
+ numregex = re.compile('([0-9]+)')
28
+
29
+ def naturalize_string(key):
30
+ """Analyzes string in a human way to enable natural sort
31
+
32
+ :param key: string for the split
33
+ :returns: A structure that can be consumed by 'sorted'
34
+ """
35
+ return [int(text) if text.isdigit() else text.lower()
36
+ for text in re.split(numregex, key)]
37
+
38
+ def natural_sort(iterable):
39
+ """Return a sort using natural sort if possible
40
+
41
+ :param iterable:
42
+ :return:
43
+ """
44
+ try:
45
+ return sorted(iterable, key=naturalize_string)
46
+ except TypeError:
47
+ # The natural sort attempt failed, fallback to ascii sort
48
+ return sorted(iterable)
24
49
 
25
50
  class SensorReading(object):
26
51
  def __init__(self, healthinfo, sensor=None, value=None, units=None,
@@ -39,8 +64,664 @@ class SensorReading(object):
39
64
 
40
65
  class OEMHandler(generic.OEMHandler):
41
66
 
67
+ def __init__(self, sysinfo, sysurl, webclient, cache, gpool=None):
68
+ super(OEMHandler, self).__init__(sysinfo, sysurl, webclient, cache,
69
+ gpool)
70
+ self.datacache = {}
71
+
72
+
42
73
  def supports_expand(self, url):
43
74
  return True
75
+
76
+ def weblogout(self):
77
+ if self.webclient:
78
+ try:
79
+ self.webclient.grab_json_response('/logout')
80
+ except Exception:
81
+ pass
82
+
83
+ def get_cached_data(self, attribute, age=30):
84
+ try:
85
+ kv = self.datacache[attribute]
86
+ if kv[1] > util._monotonic_time() - age:
87
+ return kv[0]
88
+ except KeyError:
89
+ return None
90
+
91
+ def get_inventory(self, withids=False):
92
+ sysinfo = {
93
+ 'UUID': self._varsysinfo.get('UUID', '').lower(),
94
+ 'Serial Number': self._varsysinfo.get('SerialNumber', ''),
95
+ 'Manufacturer': self._varsysinfo.get('Manufacturer', ''),
96
+ 'Product name': self._varsysinfo.get('Model', ''),
97
+ 'Model': self._varsysinfo.get(
98
+ 'SKU', self._varsysinfo.get('PartNumber', '')),
99
+ }
100
+ yield ('System', sysinfo)
101
+ for cpuinv in self._get_cpu_inventory():
102
+ yield cpuinv
103
+ for meminv in self._get_mem_inventory():
104
+ yield meminv
105
+ hwmap = self.hardware_inventory_map()
106
+ for key in natural_sort(hwmap):
107
+ yield (key, hwmap[key])
108
+
109
+ def hardware_inventory_map(self):
110
+ hwmap = self.get_cached_data('lenovo_cached_hwmap')
111
+ if hwmap:
112
+ return hwmap
113
+ hwmap = {}
114
+ for disk in self.disk_inventory(mode=1): # hardware mode
115
+ hwmap[disk[0]] = disk[1]
116
+ adapterdata = self.get_cached_data('lenovo_cached_adapters')
117
+ if not adapterdata:
118
+ # if self.updating:
119
+ # raise pygexc.TemporaryError(
120
+ # 'Cannot read extended inventory during firmware update')
121
+ if self.webclient:
122
+ adapterdata = list(self._do_bulk_requests([i['@odata.id'] for i in self.webclient.grab_json_response(
123
+ '/redfish/v1/Chassis/1')['Links']['PCIeDevices']]))
124
+ if adapterdata:
125
+ self.datacache['lenovo_cached_adapters'] = (
126
+ adapterdata, util._monotonic_time())
127
+ if adapterdata:
128
+ anames = {}
129
+ for adata, _ in adapterdata:
130
+ skipadapter = False
131
+ clabel = adata['Slot']['Location']['PartLocation']['LocationType']
132
+ if clabel != 'Embedded':
133
+ aslot = adata['Slot']['Location']['PartLocation']['LocationOrdinalValue']
134
+ clabel = 'Slot {0}'.format(aslot)
135
+ aname = adata['Name']
136
+ bdata = {'location': clabel, 'name': aname}
137
+ if aname in anames:
138
+ anames[aname] += 1
139
+ aname = '{0} {1}'.format(aname, anames[aname])
140
+ else:
141
+ anames[aname] = 1
142
+ pcislot = adata['Id'].split('_')[1]
143
+ bdata['pcislot'] = '{0}:{1}.{2}'.format(
144
+ pcislot[:4].replace('0x',''), pcislot[4:6], pcislot[6:8]
145
+ )
146
+ serialdata = adata.get('SerialNumber', '')
147
+ if (serialdata and serialdata != 'N/A'
148
+ and '---' not in serialdata):
149
+ bdata['serial'] = serialdata
150
+ partnum = adata.get('PartNumber', '')
151
+ if partnum and partnum != 'N/A':
152
+ bdata['Part Number'] = partnum
153
+ fundata = self._get_expanded_data(adata['PCIeFunctions']['@odata.id'])
154
+ venid = fundata['Members'][0].get('VendorId', None)
155
+ if venid is not None:
156
+ bdata['PCI Vendor ID'] = venid.lower().split('0x')[-1]
157
+ devid = fundata['Members'][0].get('DeviceId', None)
158
+ if devid is not None and 'PCIE Device ID' not in bdata:
159
+ bdata['PCI Device ID'] = devid.lower().split('0x')[-1]
160
+ subvenid = fundata['Members'][0].get('SubsystemVendorId', None)
161
+ if subvenid is not None:
162
+ bdata['PCI Subsystem Vendor ID'] = subvenid.lower().split('0x')[-1]
163
+ subdevid = fundata['Members'][0].get('SubsystemId', None)
164
+ if subdevid is not None:
165
+ bdata['PCI Subsystem Device ID'] = subdevid.lower().split('0x')[-1]
166
+ bdata['FRU Number'] = adata.get('SKU', '')
167
+
168
+ # Could be identified also through Oem->Lenovo->FunctionClass
169
+ if fundata['Members'][0]['DeviceClass'] == 'NetworkController':
170
+ ports_data = self._get_expanded_data('{0}/Ports'.format(adata['@odata.id'].replace('PCIeDevices','NetworkAdapters')))
171
+ macidx = 1
172
+ for port in ports_data['Members']:
173
+ if port.get('Ethernet', None):
174
+ macs = [x for x in port['Ethernet'].get('AssociatedMACAddresses', [])]
175
+ for mac in macs:
176
+ label = 'MAC Address {}'.format(macidx)
177
+ bdata[label] = generic._normalize_mac(mac)
178
+ macidx += 1
179
+ ibinfo = port.get('InfiniBand', {})
180
+ if ibinfo:
181
+ macs = [x for x in ibinfo.get('AssociatedPortGUIDs', [])]
182
+ for mac in macs:
183
+ label = 'Port GUID {}'.format(macidx)
184
+ bdata[label] = mac
185
+ macidx += 1
186
+ hwmap[aname] = bdata
187
+ self.datacache['lenovo_cached_hwmap'] = (hwmap,
188
+ util._monotonic_time())
189
+ # self.weblogout()
190
+ return hwmap
191
+
192
+ def get_disk_firmware(self, diskent, prefix=''):
193
+ bdata = {}
194
+ if not prefix:
195
+ location = diskent.get('Name', '')
196
+ if location.startswith('M.2'):
197
+ prefix = 'M.2-'
198
+ elif location.startswith('7MM'):
199
+ prefix = '7MM-'
200
+ diskname = 'Disk {0}{1}'.format(prefix, diskent['PhysicalLocation']['PartLocation']['LocationOrdinalValue'])
201
+ bdata['model'] = '{0}_{1}'.format(diskent['Manufacturer'].rstrip(), diskent['Model'].rstrip())
202
+ bdata['version'] = diskent.get('FirmwareVersion','')
203
+ return (diskname, bdata)
204
+
205
+ def get_disk_hardware(self, diskent, prefix=''):
206
+ bdata = {}
207
+ if not prefix:
208
+ location = diskent.get('Name', '')
209
+ if location.startswith('M.2'):
210
+ prefix = 'M.2-'
211
+ elif location.startswith('7MM'):
212
+ prefix = '7MM-'
213
+ diskname = 'Disk {0}{1}'.format(prefix, diskent['PhysicalLocation']['PartLocation']['LocationOrdinalValue'])
214
+ bdata['Model'] = '{0}_{1}'.format(diskent['Manufacturer'].rstrip(), diskent['Model'].rstrip())
215
+ bdata['Serial Number'] = diskent['SerialNumber'].rstrip()
216
+ bdata['FRU Number'] = diskent['SKU'].rstrip()
217
+ bdata['Description'] = diskent['Oem']['Lenovo']['TypeString'].rstrip()
218
+ return (diskname, bdata)
219
+
220
+ def disk_inventory(self, mode=0):
221
+ # mode 0 is firmware, 1 is hardware
222
+ storagedata = self.get_cached_data('lenovo_cached_storage')
223
+ if not storagedata:
224
+ if self.webclient:
225
+ storagedata = self._do_bulk_requests([i['@odata.id'] for i in self.webclient.grab_json_response(
226
+ '/redfish/v1/Chassis/1')['Links']['Drives']])
227
+ if storagedata:
228
+ self.datacache['lenovo_cached_storage'] = (
229
+ storagedata, util._monotonic_time())
230
+ # Unmanaged disks cannot be retrieved through Redfish API
231
+ if storagedata:
232
+ for diskent, _ in storagedata:
233
+ if mode == 0:
234
+ yield self.get_disk_firmware(diskent)
235
+ elif mode == 1:
236
+ yield self.get_disk_hardware(diskent)
237
+
238
+ def get_storage_configuration(self, logout=True):
239
+ rsp = self._get_expanded_data("/redfish/v1/Systems/1/Storage")
240
+ standalonedisks = []
241
+ pools = []
242
+ for item in rsp.get('Members',[]):
243
+ cdisks = [item['Drives'][i]['@odata.id'] for i in range(len(item['Drives']))]
244
+ cid = '{0},{1}'.format(
245
+ item['Id'],
246
+ item['StorageControllers'][0]['Location']['PartLocation'].get('LocationOrdinalValue', -1))
247
+ storage_pools = self._get_expanded_data(item['StoragePools']['@odata.id'])
248
+ for p in storage_pools['Members']:
249
+ vols = self._get_expanded_data(p['AllocatedVolumes']['@odata.id'])
250
+ for vol in vols['Members']:
251
+ volumes=[]
252
+ disks=[]
253
+ spares=[]
254
+ volumes.append(
255
+ storage.Volume(name=vol['DisplayName'],
256
+ size=int(vol['CapacityBytes'])/1024//1024,
257
+ status=vol['Status']['Health'],
258
+ id=(cid,vol['Id'])))
259
+ for item_key, disk_ids in vol['Links'].items():
260
+ if isinstance(disk_ids, list) and 'drives' in item_key.lower():
261
+ for disk in disk_ids:
262
+ if disk['@odata.id'] in cdisks:
263
+ cdisks.remove(disk['@odata.id'])
264
+ disk_data = self.webclient.grab_json_response(disk['@odata.id'])
265
+ (spares if disk_data['Oem']['Lenovo']['DriveStatus']=="DedicatedHotspare" else disks).append(
266
+ storage.Disk(
267
+ name=disk_data['Name'], description=disk_data['Oem']['Lenovo']['TypeString'],
268
+ id=(cid, disk_data['Id']), status=disk_data['Oem']['Lenovo']['DriveStatus'],
269
+ serial=disk_data['SerialNumber'], fru=disk_data['SKU']))
270
+ raid=vol['RAIDType']
271
+ totalsize = int(p['Capacity']['Data']['AllocatedBytes'])/1024//1024
272
+ freesize = totalsize - int(p['Capacity']['Data']['ConsumedBytes'])/1024//1024
273
+ pools.append(storage.Array(
274
+ disks=disks, raid=raid, volumes=volumes,
275
+ id=(cid, p['Id']), hotspares=spares,
276
+ capacity=totalsize, available_capacity=freesize))
277
+ for d in cdisks:
278
+ disk_data = self.webclient.grab_json_response(d)
279
+ standalonedisks.append(
280
+ storage.Disk(
281
+ name=disk_data['Name'], description=disk_data['Oem']['Lenovo']['TypeString'],
282
+ id=(cid, disk_data['Id']), status=disk_data['Oem']['Lenovo']['DriveStatus'],
283
+ serial=disk_data['SerialNumber'], fru=disk_data['SKU']))
284
+ return storage.ConfigSpec(disks=standalonedisks, arrays=pools)
285
+
286
+ def check_storage_configuration(self, cfgspec=None):
287
+ rsp = self.webclient.grab_json_response(
288
+ '/api/providers/raidlink_GetStatus')
289
+ if rsp['return'] != 0 or rsp['status'] != 1:
290
+ raise pygexc.TemporaryError('Storage configuration unavailable in '
291
+ 'current state (try boot to setup or '
292
+ 'an OS)')
293
+ return True
294
+
295
+ def apply_storage_configuration(self, cfgspec):
296
+ realcfg = self.get_storage_configuration(False)
297
+ for disk in cfgspec.disks:
298
+ if disk.status.lower() == 'jbod':
299
+ self._make_jbod(disk, realcfg)
300
+ elif disk.status.lower() == 'hotspare':
301
+ self._make_global_hotspare(disk, realcfg)
302
+ elif disk.status.lower() in ('unconfigured', 'available', 'ugood',
303
+ 'unconfigured good'):
304
+ self._make_available(disk, realcfg)
305
+ for pool in cfgspec.arrays:
306
+ if pool.disks:
307
+ self._create_array(pool)
308
+
309
+ def _make_available(self, disk, realcfg):
310
+ currstatus = self._get_status(disk, realcfg)
311
+ newstate = None
312
+ if currstatus.lower() == 'unconfiguredgood':
313
+ return
314
+ elif currstatus.lower() == 'globalhotspare':
315
+ newstate = "None"
316
+ elif currstatus.lower() == 'jbod':
317
+ newstate = "MakeUnconfiguredGood"
318
+ self._set_drive_state(disk, newstate)
319
+
320
+ def _make_jbod(self, disk, realcfg):
321
+ currstatus = self._get_status(disk, realcfg)
322
+ if currstatus.lower() == 'jbod':
323
+ return
324
+ self._make_available(disk, realcfg)
325
+ self._set_drive_state(disk, "MakeJBOD")
326
+
327
+ def _make_global_hotspare(self, disk, realcfg):
328
+ currstatus = self._get_status(disk, realcfg)
329
+ if currstatus.lower() == 'globalhotspare':
330
+ return
331
+ self._make_available(disk, realcfg)
332
+ self._set_drive_state(disk, "Global")
333
+
334
+ def _set_drive_state(self, disk, state):
335
+ raid_alldevices = self.webclient.grab_json_response(
336
+ '/api/providers/raid_alldevices')
337
+ if raid_alldevices.get('return', -1) != 0:
338
+ raise Exception(
339
+ 'Unexpected return to get all RAID devices information')
340
+ for c in raid_alldevices.get('StorageComplexes',[]):
341
+ cslot = str(c.get('SlotNumber'))
342
+ if cslot == disk.id[0].split(',')[1]:
343
+ c_pciaddr = c.get('PCIeAddress',-1)
344
+ cdrives = c.get('Drives',[])
345
+ for d in cdrives:
346
+ if disk.id[1] == d.get('Id',''):
347
+ currstatus = d['Oem']['Lenovo']['DriveStatus']
348
+ d_resid = d['Internal']['ResourceId']
349
+ if state in ("Global", "None"):
350
+ data = {
351
+ "controller_address": c_pciaddr,
352
+ "drive_resource_id": d_resid,
353
+ "hotspare_type": state,
354
+ "pool_resource_ids": []}
355
+ raidlink_url = '/api/providers/raidlink_AssignHotSpare'
356
+ else:
357
+ data = {
358
+ "controller_address": c_pciaddr,
359
+ "drive_operation": state,
360
+ "drive_resource_ids": [d_resid]}
361
+ raidlink_url = '/api/providers/raidlink_DiskStateAction'
362
+ msg = self._do_web_request(raidlink_url, method='POST',
363
+ payload=data, cache=False)
364
+ if msg.get('return', -1) != 0:
365
+ raise Exception(
366
+ 'Unexpected return to set disk state: {0}'.format(
367
+ msg.get('return', -1)))
368
+ set_state_token = msg.get('token', '')
369
+ msg = self._do_web_request(
370
+ '/api/providers/raidlink_QueryAsyncStatus',
371
+ method='POST',
372
+ payload={"token": set_state_token},
373
+ cache=False)
374
+ while msg['status'] == 2:
375
+ time.sleep(1)
376
+ msg = self._do_web_request(
377
+ '/api/providers/raidlink_QueryAsyncStatus',
378
+ method='POST',
379
+ payload={"token": set_state_token},
380
+ cache=False)
381
+ if msg.get('return',-1) != 0 or msg.get('status',-1) != 0:
382
+ raise Exception(
383
+ 'Unexpected return to set disk state: {0}'.format(
384
+ msg.get('return', -1)))
385
+ disk_url=f"/redfish/v1/Systems/1/Storage/{disk.id[0].split(',')[0]}/Drives/{disk.id[1]}"
386
+ newstatus = self.webclient.grab_json_response(disk_url)
387
+ disk_converted = False
388
+ for _ in range(60):
389
+ if currstatus == newstatus['Oem']['Lenovo']['DriveStatus']:
390
+ time.sleep(1)
391
+ newstatus = self.webclient.grab_json_response(disk_url)
392
+ else:
393
+ disk_converted = True
394
+ break
395
+ if not disk_converted:
396
+ raise Exception(
397
+ 'Disk set command was successful, but the disk state is unchanged')
398
+
399
+ def _get_status(self, disk, realcfg):
400
+ for cfgdisk in realcfg.disks:
401
+ if disk.id == cfgdisk.id:
402
+ currstatus = cfgdisk.status
403
+ break
404
+ else:
405
+ raise pygexc.InvalidParameterValue('Requested disk not found')
406
+ return currstatus
407
+
408
+ def remove_storage_configuration(self, cfgspec):
409
+ realcfg = self.get_storage_configuration(False)
410
+ for pool in cfgspec.arrays:
411
+ for volume in pool.volumes:
412
+ cid = volume.id[0].split(',')[0]
413
+ vid = volume.id[1]
414
+ msg, code = self.webclient.grab_json_response_with_status(
415
+ f'/redfish/v1/Systems/1/Storage/{cid}/Volumes/{vid}',
416
+ method='DELETE')
417
+ if code == 500:
418
+ raise Exception(
419
+ 'Unexpected return to volume deletion: ' + repr(msg))
420
+ for disk in cfgspec.disks:
421
+ self._make_available(disk, realcfg)
422
+
423
+ def _parse_array_spec(self, arrayspec):
424
+ controller = None
425
+ if arrayspec.disks:
426
+ for disk in list(arrayspec.disks) + list(arrayspec.hotspares):
427
+ if controller is None:
428
+ controller = disk.id[0]
429
+ if controller != disk.id[0]:
430
+ raise pygexc.UnsupportedFunctionality(
431
+ 'Cannot span arrays across controllers')
432
+ raidmap = self._raid_number_map(controller)
433
+ if not raidmap:
434
+ raise pygexc.InvalidParameterValue(
435
+ 'No RAID Type supported on this controller')
436
+ requestedlevel = str(arrayspec.raid)
437
+ if requestedlevel not in raidmap:
438
+ raise pygexc.InvalidParameterValue(
439
+ 'Requested RAID Type "{0}" not available on this '
440
+ 'controller. Allowed values are: {1}'.format(
441
+ requestedlevel, [k for k in raidmap]))
442
+ rdinfo = raidmap[str(arrayspec.raid).lower()]
443
+ rdlvl = str(rdinfo[0])
444
+ defspan = 1 if rdinfo[1] == 1 else 2
445
+ spancount = defspan if arrayspec.spans is None else arrayspec.spans
446
+ drivesperspan = str(len(arrayspec.disks) // int(spancount))
447
+ hotspares = arrayspec.hotspares
448
+ drives = arrayspec.disks
449
+ minimal_conditions = {
450
+ "RAID0": (1,128,1),
451
+ "RAID1": (2,2,1),
452
+ "RAID1Triple": (3,3,1),
453
+ "RAID10": (4,128,2),
454
+ "RAID10Triple": (6,128,3),
455
+ "RAID5": (3,128,1),
456
+ "RAID50": (6,128,2),
457
+ "RAID6": (4,128,1),
458
+ "RAID60": (8, 128, 2)
459
+ }
460
+ raid_level = rdinfo[0]
461
+ min_pd = minimal_conditions[raid_level][0]
462
+ max_pd = minimal_conditions[raid_level][1]
463
+ disk_multiplier = minimal_conditions[raid_level][2]
464
+ if len(drives) < min_pd or \
465
+ len(drives) > max_pd or \
466
+ len(drives) % disk_multiplier != 0:
467
+ raise pygexc.InvalidParameterValue(
468
+ f'Number of disks for {rdinfo} must be between {min_pd} and {max_pd} and be a multiple of {disk_multiplier}')
469
+ if hotspares:
470
+ hstr = '|'.join([str(x.id[1]) for x in hotspares]) + '|'
471
+ else:
472
+ hstr = ''
473
+ drvstr = '|'.join([str(x.id[1]) for x in drives]) + '|'
474
+ return {
475
+ 'controller': controller,
476
+ 'drives': drvstr,
477
+ 'hotspares': hstr,
478
+ 'raidlevel': rdlvl,
479
+ 'spans': spancount,
480
+ 'perspan': drivesperspan,
481
+ }
482
+ else:
483
+ # TODO(Jarrod Johnson): adding new volume to
484
+ # existing array would be here
485
+ pass
486
+
487
+ def _raid_number_map(self, controller):
488
+ themap = {}
489
+ cid = controller.split(',')
490
+ rsp = self.webclient.grab_json_response(
491
+ '/redfish/v1/Systems/1/Storage/{0}'.format(cid[0]))
492
+ for rt in rsp['StorageControllers'][0]['SupportedRAIDTypes']:
493
+ rt_lower = rt.lower()
494
+ mapdata = (rt, 1)
495
+ themap[rt]=mapdata
496
+ themap[rt_lower] = mapdata
497
+ themap[rt_lower.replace('raid','r')] = mapdata
498
+ themap[rt_lower.replace('raid','')] = mapdata
499
+ return themap
500
+
501
+ def _create_array(self, pool):
502
+ params = self._parse_array_spec(pool)
503
+ cid = params['controller'].split(',')[0]
504
+ c_capabilities, code = self.webclient.grab_json_response_with_status(
505
+ f'/redfish/v1/Systems/1/Storage/{cid}/Volumes/Capabilities')
506
+ if code == 404:
507
+ c_capabilities, code = self.webclient.grab_json_response_with_status(
508
+ f'/redfish/v1/Systems/1/Storage/{cid}/Volumes/Oem/Lenovo/Capabilities')
509
+ if code == 404:
510
+ # If none of the endpoints exist, maybe it should be printed that
511
+ # no capabilities found, therefore default values will be used
512
+ # whatever they are
513
+ pass
514
+ volumes = pool.volumes
515
+ drives = [d for d in params['drives'].split("|") if d != '']
516
+ hotspares = [h for h in params['hotspares'].split("|") if h != '']
517
+ raidlevel = params['raidlevel']
518
+ nameappend = 1
519
+ currvolnames = None
520
+ currcfg = self.get_storage_configuration(False)
521
+ for vol in volumes:
522
+ if vol.name is None:
523
+ # need to iterate while there exists a volume of that name
524
+ if currvolnames is None:
525
+ currvolnames = set([])
526
+ for pool in currcfg.arrays:
527
+ for volume in pool.volumes:
528
+ currvolnames.add(volume.name)
529
+ name = 'Volume_{0}'.format(nameappend)
530
+ nameappend += 1
531
+ while name in currvolnames:
532
+ name = 'Volume_{0}'.format(nameappend)
533
+ nameappend += 1
534
+ else:
535
+ name = vol.name
536
+
537
+ # Won't check against Redfish allowable values as they not trustworthy yet
538
+ # Some values show in Redfish, but may not be accepted by UEFI/controller or vice versa
539
+ stripsize_map = {
540
+ '4': 4096, '4096': 4096,
541
+ '16': 16384, '16384': 16384,
542
+ '32': 32768, '32768': 32768,
543
+ '64': 65536, '65536': 65536,
544
+ '128': 131072, '131072': 131072,
545
+ '256': 262144, '262144': 262144,
546
+ '512': 524288, '524288': 524288,
547
+ '1024': 1048576, '1048576': 1048576
548
+ }
549
+ stripsize = stripsize_map[str(vol.stripsize).lower().replace('k','')] if vol.stripsize is not None else None
550
+
551
+ readpolicy_map = {'0': 'Off', '1': 'ReadAhead'}
552
+ read_policy = None
553
+ read_cache_possible = c_capabilities.get("ReadCachePolicy@Redfish.AllowableValues",[])
554
+ if read_cache_possible:
555
+ if vol.read_policy is not None:
556
+ if str(vol.read_policy) in readpolicy_map:
557
+ vol.read_policy = readpolicy_map[str(vol.read_policy)]
558
+ if vol.read_policy in read_cache_possible:
559
+ read_policy = vol.read_policy
560
+ else:
561
+ raise pygexc.InvalidParameterValue(
562
+ f'{vol.read_policy} Read Cache Policy is not supported. Allowed values are: {read_cache_possible}')
563
+
564
+ writepolicy_map = {'0': 'WriteThrough', '1': 'UnprotectedWriteBack',
565
+ '2': 'ProtectedWriteBack', '3': 'Off'}
566
+ write_policy = None
567
+ write_cache_possible = c_capabilities.get("WriteCachePolicy@Redfish.AllowableValues",[])
568
+ if write_cache_possible:
569
+ if vol.write_policy is not None:
570
+ if str(vol.write_policy) in writepolicy_map:
571
+ vol.write_policy = writepolicy_map[str(vol.write_policy)]
572
+ if vol.write_policy in write_cache_possible:
573
+ write_policy = vol.write_policy
574
+ else:
575
+ raise pygexc.InvalidParameterValue(
576
+ f'{vol.write_policy} Write Cache Policy is not supported. Allowed values are: {write_cache_possible}')
577
+
578
+ defaultinit_map = {'0': 'No', '1': 'Fast', '2': 'Full'}
579
+ default_init = None
580
+ default_init_possible = c_capabilities.get("InitializationType@Redfish.AllowableValues",[])
581
+ if default_init_possible:
582
+ if vol.default_init is not None:
583
+ if str(vol.default_init) in defaultinit_map:
584
+ vol.default_init = defaultinit_map[str(vol.default_init)]
585
+ if vol.default_init in default_init_possible:
586
+ default_init = vol.default_init
587
+ else:
588
+ raise pygexc.InvalidParameterValue(
589
+ f'{vol.default_init} Initialization Type is not supported. Allowed values are: {default_init_possible}')
590
+
591
+ volsize = None
592
+ spec_disks = sorted(drives)
593
+ spec_hotspares = hotspares
594
+ for array in currcfg.arrays:
595
+ in_use_disks = sorted([d.id[1] for d in array.disks])
596
+ in_use_hotspares = [h.id[1] for h in array.hotspares]
597
+ if spec_disks == in_use_disks:
598
+ if vol.size is None:
599
+ volsize = array.available_capacity
600
+ array.available_capacity = 0
601
+ break
602
+ else:
603
+ strsize = str(vol.size)
604
+ if strsize in ('all','100%'):
605
+ raise pygexc.InvalidParameterValue(
606
+ f'Requested size for volume {name} exceeds available capacity. Available capacity is {array.available_capacity} MiB')
607
+ elif strsize.endswith('%'):
608
+ volsize = int(array.capacity
609
+ * float(strsize.replace('%', ''))
610
+ / 100.0)
611
+ if volsize > array.available_capacity:
612
+ raise pygexc.InvalidParameterValue(
613
+ f'Requested size for volume {name} exceeds available capacity. Available capacity is {array.available_capacity} MiB')
614
+ else:
615
+ array.available_capacity-=volsize
616
+ else:
617
+ try:
618
+ volsize = int(strsize)
619
+ if volsize > array.available_capacity:
620
+ raise pygexc.InvalidParameterValue(
621
+ f'Requested size for volume {name} exceeds available capacity. Available capacity is {array.available_capacity} MiB')
622
+ else:
623
+ array.available_capacity-=volsize
624
+ except ValueError:
625
+ raise pygexc.InvalidParameterValue(
626
+ 'Unrecognized size ' + strsize)
627
+ elif any(d in in_use_disks for d in spec_disks):
628
+ raise pygexc.InvalidParameterValue(
629
+ f'At least one disk from provided config is in use by another volume. To create a volume using the remaining capacity, configure the new volume to use all the following disks: {in_use_disks}')
630
+ else:
631
+ disks_capacities = {}
632
+ for d in spec_disks:
633
+ disks_capacities[d] = self.webclient.grab_json_response(
634
+ f'/redfish/v1/Systems/1/Storage/{cid}/Drives/{d}')["CapacityBytes"]
635
+ max_capacity = sum(v for k,v in disks_capacities.items())
636
+ min_disk = min([v for k,v in disks_capacities.items()])
637
+ disk_count = len(disks_capacities)
638
+ max_capacity_per_raid = {
639
+ "RAID0": max_capacity,
640
+ "RAID1": min_disk,
641
+ "RAID1Triple": min_disk,
642
+ "RAID10": (disk_count//2)*min_disk,
643
+ "RAID10Triple": (disk_count//3)*min_disk,
644
+ "RAID5": (disk_count-1)*min_disk,
645
+ "RAID50": (disk_count-2)*min_disk,
646
+ "RAID6": (disk_count-2)*min_disk,
647
+ "RAID60": (disk_count-4)*min_disk
648
+ }
649
+ if vol.size is not None:
650
+ strsize = str(vol.size)
651
+ if strsize.endswith('%'):
652
+ volsize = int(max_capacity_per_raid[raidlevel]
653
+ * float(strsize.replace('%', ''))
654
+ / 100.0)
655
+ else:
656
+ try:
657
+ volsize = int(strsize)
658
+ if volsize > max_capacity_per_raid[raidlevel]:
659
+ raise pygexc.InvalidParameterValue(
660
+ f'Requested size for volume {name} exceeds available capacity. Available capacity is {max_capacity_per_raid[raidlevel]} bytes')
661
+ except ValueError:
662
+ raise pygexc.InvalidParameterValue(
663
+ 'Unrecognized size ' + strsize)
664
+ for h in spec_hotspares:
665
+ if h in in_use_hotspares:
666
+ raise pygexc.InvalidParameterValue(
667
+ f'Hotspare {h} from provided config is in use by another volume.')
668
+
669
+ request_data = {
670
+ "Name":name,
671
+ "RAIDType":raidlevel,
672
+ "Links":{
673
+ "Drives":[
674
+ {'@odata.id': f'/redfish/v1/Systems/1/Storage/{cid}/Drives/{did}'} for did in spec_disks]}}
675
+ if spec_hotspares:
676
+ request_data["Links"]["DedicatedSpareDrives"] = {[
677
+ {'@odata.id': f'/redfish/v1/Systems/1/Storage/{cid}/Drives/{hid}' for hid in spec_hotspares}]}
678
+ if volsize:
679
+ request_data["CapacityBytes"] = volsize
680
+ if stripsize:
681
+ request_data["StripSizeBytes"] = stripsize
682
+ if read_policy:
683
+ request_data["ReadCachePolicy"] = read_policy
684
+ if write_policy:
685
+ request_data["WriteCachePolicy"] = write_policy
686
+
687
+ msg, code=self.webclient.grab_json_response_with_status(
688
+ f'/redfish/v1/Systems/1/Storage/{cid}/Volumes',
689
+ method='POST',
690
+ data=request_data)
691
+ if code == 500:
692
+ raise Exception("Unexpected response to volume creation: " + repr(msg))
693
+ time.sleep(60)
694
+ #Even if in web the volume appears immediately, get_storage_configuration does not see it that fast
695
+ newcfg = self.get_storage_configuration(False)
696
+ newvols = [v.id for p in newcfg.arrays for v in p.volumes]
697
+ currvols = [v.id for p in currcfg.arrays for v in p.volumes]
698
+ newvol = list(set(newvols) - set(currvols))[0]
699
+ if default_init:
700
+ msg, code = self.webclient.grab_json_response_with_status(
701
+ f'/redfish/v1/Systems/1/Storage/{cid}/Volumes/{newvol[1]}/Actions/Volume.Initialize',
702
+ method='POST',
703
+ data = {"InitializeType": default_init})
704
+ if code == 500:
705
+ raise Exception("Unexpected response to volume initialization: " + repr(msg))
706
+
707
+ def attach_remote_media(self, url, user, password, vmurls):
708
+ for vmurl in vmurls:
709
+ if 'EXT' not in vmurl:
710
+ continue
711
+ vminfo = self._do_web_request(vmurl, cache=False)
712
+ if vminfo['ConnectedVia'] != 'NotConnected':
713
+ continue
714
+ msg,code = self.webclient.grab_json_response_with_status(
715
+ vmurl,
716
+ data={'Image': url, 'Inserted': True},
717
+ method='PATCH')
718
+ if code == 500:
719
+ raise Exception("Unexpected response when attaching remote media: " + repr(msg))
720
+ raise pygexc.BypassGenericBehavior()
721
+ break
722
+ else:
723
+ raise pygexc.InvalidParameterValue(
724
+ 'XCC does not have required license for operation')
44
725
 
45
726
  def get_screenshot(self, outfile):
46
727
  wc = self.webclient.dupe()
@@ -260,11 +941,15 @@ class OEMHandler(generic.OEMHandler):
260
941
  src, dst = currval.split(',')
261
942
  mappings.append('{}:{}'.format(src,dst))
262
943
  settings['usb_forwarded_ports'] = {'value': ','.join(mappings)}
944
+ cfgin = self._get_lnv_bmcstgs(self)[0]
945
+ for stgname in cfgin:
946
+ settings[f'{stgname}'] = cfgin[stgname]
263
947
  return settings
264
948
 
265
949
  def set_bmc_configuration(self, changeset):
266
950
  acctattribs = {}
267
951
  usbsettings = {}
952
+ bmchangeset = {}
268
953
  for key in changeset:
269
954
  if isinstance(changeset[key], str):
270
955
  changeset[key] = {'value': changeset[key]}
@@ -299,14 +984,15 @@ class OEMHandler(generic.OEMHandler):
299
984
  'usb_forwarded_ports'):
300
985
  usbsettings[key] = currval
301
986
  else:
302
- raise pygexc.InvalidParameterValue(
303
- '{0} not a known setting'.format(key))
987
+ bmchangeset[key.replace('bmc.', '')] = changeset[key]
304
988
  if acctattribs:
305
989
  self._do_web_request(
306
990
  '/redfish/v1/AccountService', acctattribs, method='PATCH')
307
991
  self._do_web_request('/redfish/v1/AccountService', cache=False)
308
992
  if usbsettings:
309
993
  self.apply_usb_configuration(usbsettings)
994
+ if bmchangeset:
995
+ self._set_xcc3_settings(bmchangeset, self)
310
996
 
311
997
  def apply_usb_configuration(self, usbsettings):
312
998
  bmcattribs = {}
@@ -480,5 +1166,3 @@ class OEMHandler(generic.OEMHandler):
480
1166
  'Name': 'HPM-FPGA Pending',
481
1167
  'build': pendinghpm}
482
1168
  raise pygexc.BypassGenericBehavior()
483
-
484
-
pyghmi/util/webclient.py CHANGED
@@ -96,7 +96,7 @@ class FileDownloader(threading.Thread):
96
96
  self.exc = e
97
97
 
98
98
 
99
- def get_upload_form(filename, data, formname, otherfields):
99
+ def get_upload_form(filename, data, formname, otherfields, boundary=BND):
100
100
  ffilename = filename.split('/')[-1]
101
101
  if not formname:
102
102
  formname = ffilename
@@ -114,16 +114,16 @@ def get_upload_form(filename, data, formname, otherfields):
114
114
  if isinstance(tfield, dict):
115
115
  tfield = json.dumps(tfield)
116
116
  xtra = '\r\nContent-Type: application/json'
117
- form += (b'--' + BND
117
+ form += (b'--' + boundary
118
118
  + '\r\nContent-Disposition: form-data; '
119
119
  'name="{0}"{1}\r\n\r\n{2}\r\n'.format(
120
120
  ofield, xtra, tfield).encode('utf-8'))
121
- form += (b'--' + BND
121
+ form += (b'--' + boundary
122
122
  + '\r\nContent-Disposition: form-data; '
123
123
  'name="{0}"; filename="{1}"\r\n'.format(
124
124
  formname, ffilename).encode('utf-8'))
125
125
  form += b'Content-Type: application/octet-stream\r\n\r\n' + data
126
- form += b'\r\n--' + BND + b'--\r\n'
126
+ form += b'\r\n--' + boundary + b'--\r\n'
127
127
  uploadforms[filename] = form
128
128
  return form
129
129
 
@@ -332,13 +332,14 @@ class SecureHTTPConnection(httplib.HTTPConnection, object):
332
332
  the file.
333
333
  :return:
334
334
  """
335
+ boundary = base64.b64encode(os.urandom(54))[:70]
335
336
  if data is None:
336
337
  data = open(filename, 'rb')
337
338
  ulhdrs = self.stdheaders.copy()
338
339
  if formwrap:
339
340
  self._upbuffer = io.BytesIO(get_upload_form(
340
- filename, data, formname, otherfields))
341
- ulhdrs['Content-Type'] = b'multipart/form-data; boundary=' + BND
341
+ filename, data, formname, otherfields, boundary))
342
+ ulhdrs['Content-Type'] = b'multipart/form-data; boundary=' + boundary
342
343
  ulhdrs['Content-Length'] = len(uploadforms[filename])
343
344
  self.ulsize = len(uploadforms[filename])
344
345
  else:
@@ -1,6 +1,7 @@
1
1
  Akira Yoshiyama <akirayoshiyama@gmail.com>
2
2
  Allan Vidal <avidal@lenovo.com>
3
3
  Andreas Jaeger <aj@suse.com>
4
+ Andrei Machedon <amachedon@lenovo.com>
4
5
  Christian Arnold <the-fr3ak@gmx.net>
5
6
  Connor Reed <conman2305@hotmail.com>
6
7
  Daniel Speichert <Daniel_Speichert@comcast.com>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyghmi
3
- Version: 1.6.1
3
+ Version: 1.6.3
4
4
  Summary: Python General Hardware Management Initiative (IPMI and others)
5
5
  Home-page: http://github.com/openstack/pyghmi/
6
6
  Author: Jarrod Johnson
@@ -38,14 +38,14 @@ pyghmi/ipmi/private/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
38
38
  pyghmi/ipmi/private/constants.py,sha256=ViuB_nXsMM5LjkT9FGf2it2KQKvEehd5cZJgewjmVds,66936
39
39
  pyghmi/ipmi/private/localsession.py,sha256=DThuwgvsAJT2AQ8byg4TA8AZG4hBBHnT-pxIJRsISCE,4879
40
40
  pyghmi/ipmi/private/serversession.py,sha256=H5toINVu7sEuc13MWJHVG3zqNCyMY1tYXjQ4ZOOjQfE,16411
41
- pyghmi/ipmi/private/session.py,sha256=276g11vswSuXxEkXaCqfBE68eStZ4KymKS5uDGHzjJQ,81797
41
+ pyghmi/ipmi/private/session.py,sha256=d6Mk6bPN-2B53dm_AQ4cVk99noEKqlGRcGJamarWzsQ,81933
42
42
  pyghmi/ipmi/private/simplesession.py,sha256=cNGaoT0uWIKDut6gUG9kAOX_b_qTzdB26R6I6Qk7cns,58834
43
43
  pyghmi/ipmi/private/spd.py,sha256=oEPSXm19X2eNXDiyW_6fVjBFqhuuMAtBI9quRJgclH4,27094
44
44
  pyghmi/ipmi/private/util.py,sha256=ayYodiSydlrrt0_pQppoRB1T6n-KNOiHZSfAlCMcpG0,3847
45
45
  pyghmi/redfish/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
46
46
  pyghmi/redfish/command.py,sha256=0mLgsynfiyfi_i4wtL3ZqPR6CeeHajj-azqTW7EUE3I,61278
47
47
  pyghmi/redfish/oem/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
- pyghmi/redfish/oem/generic.py,sha256=5RalOmcBlnvVx05sgo9xCYkTVZ8CrhKMb7pDTtuLAls,56718
48
+ pyghmi/redfish/oem/generic.py,sha256=OvkQV3-p6aoY-oeVNSpfChpAuh1iDOV8OXdWuz7tW9Q,56747
49
49
  pyghmi/redfish/oem/lookup.py,sha256=pfJW5xSkUY61OirMeYy0b1SbjBFz6IDfN5ZOYog_Yq4,1530
50
50
  pyghmi/redfish/oem/dell/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
51
  pyghmi/redfish/oem/dell/idrac.py,sha256=pNnmqdV1sOP3ABw0xq0wF1QEO2L8onT7Osc_-sDO8EU,2146
@@ -55,7 +55,7 @@ pyghmi/redfish/oem/lenovo/main.py,sha256=bnx8LuC_C4_OluNR8JSHIxtSlM4_jdBb4cUzJM6
55
55
  pyghmi/redfish/oem/lenovo/smm3.py,sha256=W3PKHpFNj5Ujql-neWTH3UgYGDZXQlBvC1aRX97GV8A,5982
56
56
  pyghmi/redfish/oem/lenovo/tsma.py,sha256=6GELCuriumARj_kv7fgqtUpo9ekiWHpQcM9v_mnGILI,34645
57
57
  pyghmi/redfish/oem/lenovo/xcc.py,sha256=78ksNj2-0jquj61lmAZldy3DdcR5KndqbLQ2Y4ZSFOM,84234
58
- pyghmi/redfish/oem/lenovo/xcc3.py,sha256=KBJQLAIpaU32bfFzKxZ9jH7oHnGgJIBz0bMZT-TMKy8,20989
58
+ pyghmi/redfish/oem/lenovo/xcc3.py,sha256=Uu6VjvUNZgNkAW5slEGckFR55htALbrxvVUjkIy-1g8,55678
59
59
  pyghmi/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
60
60
  pyghmi/tests/unit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
61
61
  pyghmi/tests/unit/base.py,sha256=xWImA7zPRgfrEe2xAdRZ6w_dLwExGRBJ5CBybssUQGg,744
@@ -63,12 +63,12 @@ pyghmi/tests/unit/ipmi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
63
63
  pyghmi/tests/unit/ipmi/test_sdr.py,sha256=vb3iLY0cnHJ2K_m4xgYUjEcbPd_ZYhYx-uBowByplXw,824
64
64
  pyghmi/util/__init__.py,sha256=GZLBWJiun2Plb_VE9dDSh4_PQMCha3gA7QLUqx3oSYI,25
65
65
  pyghmi/util/parse.py,sha256=6VlyBCEcE8gy8PJWmEDdtCyWATaKwPaTswCdioPCWOE,2120
66
- pyghmi/util/webclient.py,sha256=782_yMuy_LuN9E2vh2EJ-R64X_EyvLLRuurE__jfn20,15371
67
- pyghmi-1.6.1.dist-info/AUTHORS,sha256=-0iHKtdQwAJfAGKcruCnvcQXrXuE_LgBZ3P15DJI1xY,2044
68
- pyghmi-1.6.1.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
69
- pyghmi-1.6.1.dist-info/METADATA,sha256=t5NiZ7qQvKHYxH4qSU85HdVzQygxx10AKpqsUT6qk8c,1136
70
- pyghmi-1.6.1.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
71
- pyghmi-1.6.1.dist-info/entry_points.txt,sha256=-OpJliDzATxmuPXK0VR3Ma-Yk_i4ZhfIIB-12A26dSI,168
72
- pyghmi-1.6.1.dist-info/pbr.json,sha256=WluQ4jTELF6lRCdfFuR9bmRh8RujczTU1WXGdV0F-sM,46
73
- pyghmi-1.6.1.dist-info/top_level.txt,sha256=aDtt6S9eVu6-tNdaUs4Pz9PbdUd69bziZZMhNvk9Ulc,7
74
- pyghmi-1.6.1.dist-info/RECORD,,
66
+ pyghmi/util/webclient.py,sha256=Sw-XQI0udlD22pdCMOBPvv42KMtLV4a3noYY9v_Muzw,15472
67
+ pyghmi-1.6.3.dist-info/AUTHORS,sha256=yv4aQom_PII-SNqbeeKrfH-spcG85PRPsZ71Iqjl_fU,2083
68
+ pyghmi-1.6.3.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
69
+ pyghmi-1.6.3.dist-info/METADATA,sha256=nYZYXWlA0oLwsa438e0NOFvPrd_XMrR7-ZURkEK7D3E,1136
70
+ pyghmi-1.6.3.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
71
+ pyghmi-1.6.3.dist-info/entry_points.txt,sha256=-OpJliDzATxmuPXK0VR3Ma-Yk_i4ZhfIIB-12A26dSI,168
72
+ pyghmi-1.6.3.dist-info/pbr.json,sha256=vB_8gvy6-F0qrwot79vigX7I5YKQFLwLkZqGWdkJErA,46
73
+ pyghmi-1.6.3.dist-info/top_level.txt,sha256=aDtt6S9eVu6-tNdaUs4Pz9PbdUd69bziZZMhNvk9Ulc,7
74
+ pyghmi-1.6.3.dist-info/RECORD,,
@@ -0,0 +1 @@
1
+ {"git_version": "58cf3ce", "is_release": true}
@@ -1 +0,0 @@
1
- {"git_version": "33cff21", "is_release": true}
File without changes