pyghmi 1.6.1__py3-none-any.whl → 1.6.2__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/redfish/oem/generic.py +1 -0
- pyghmi/redfish/oem/lenovo/xcc3.py +664 -2
- {pyghmi-1.6.1.dist-info → pyghmi-1.6.2.dist-info}/AUTHORS +1 -0
- {pyghmi-1.6.1.dist-info → pyghmi-1.6.2.dist-info}/METADATA +1 -1
- {pyghmi-1.6.1.dist-info → pyghmi-1.6.2.dist-info}/RECORD +10 -10
- pyghmi-1.6.2.dist-info/pbr.json +1 -0
- pyghmi-1.6.1.dist-info/pbr.json +0 -1
- {pyghmi-1.6.1.dist-info → pyghmi-1.6.2.dist-info}/LICENSE +0 -0
- {pyghmi-1.6.1.dist-info → pyghmi-1.6.2.dist-info}/WHEEL +0 -0
- {pyghmi-1.6.1.dist-info → pyghmi-1.6.2.dist-info}/entry_points.txt +0 -0
- {pyghmi-1.6.1.dist-info → pyghmi-1.6.2.dist-info}/top_level.txt +0 -0
pyghmi/redfish/oem/generic.py
CHANGED
@@ -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,647 @@ class SensorReading(object):
|
|
39
64
|
|
40
65
|
class OEMHandler(generic.OEMHandler):
|
41
66
|
|
67
|
+
datacache = {}
|
68
|
+
|
42
69
|
def supports_expand(self, url):
|
43
70
|
return True
|
71
|
+
|
72
|
+
def weblogout(self):
|
73
|
+
if self.webclient:
|
74
|
+
try:
|
75
|
+
self.webclient.grab_json_response('/logout')
|
76
|
+
except Exception:
|
77
|
+
pass
|
78
|
+
|
79
|
+
def get_cached_data(self, attribute, age=30):
|
80
|
+
try:
|
81
|
+
kv = self.datacache[attribute]
|
82
|
+
if kv[1] > util._monotonic_time() - age:
|
83
|
+
return kv[0]
|
84
|
+
except KeyError:
|
85
|
+
return None
|
86
|
+
|
87
|
+
def get_inventory(self, withids=False):
|
88
|
+
sysinfo = {
|
89
|
+
'UUID': self._varsysinfo.get('UUID', '').lower(),
|
90
|
+
'Serial Number': self._varsysinfo.get('SerialNumber', ''),
|
91
|
+
'Manufacturer': self._varsysinfo.get('Manufacturer', ''),
|
92
|
+
'Product name': self._varsysinfo.get('Model', ''),
|
93
|
+
'Model': self._varsysinfo.get(
|
94
|
+
'SKU', self._varsysinfo.get('PartNumber', '')),
|
95
|
+
}
|
96
|
+
yield ('System', sysinfo)
|
97
|
+
for cpuinv in self._get_cpu_inventory():
|
98
|
+
yield cpuinv
|
99
|
+
for meminv in self._get_mem_inventory():
|
100
|
+
yield meminv
|
101
|
+
hwmap = self.hardware_inventory_map()
|
102
|
+
for key in natural_sort(hwmap):
|
103
|
+
yield (key, hwmap[key])
|
104
|
+
|
105
|
+
def hardware_inventory_map(self):
|
106
|
+
hwmap = self.get_cached_data('lenovo_cached_hwmap')
|
107
|
+
if hwmap:
|
108
|
+
return hwmap
|
109
|
+
hwmap = {}
|
110
|
+
for disk in self.disk_inventory(mode=1): # hardware mode
|
111
|
+
hwmap[disk[0]] = disk[1]
|
112
|
+
adapterdata = self.get_cached_data('lenovo_cached_adapters')
|
113
|
+
if not adapterdata:
|
114
|
+
# if self.updating:
|
115
|
+
# raise pygexc.TemporaryError(
|
116
|
+
# 'Cannot read extended inventory during firmware update')
|
117
|
+
if self.webclient:
|
118
|
+
adapterdata = self._do_bulk_requests([i['@odata.id'] for i in self.webclient.grab_json_response(
|
119
|
+
'/redfish/v1/Chassis/1')['Links']['PCIeDevices']])
|
120
|
+
if adapterdata:
|
121
|
+
self.datacache['lenovo_cached_adapters'] = (
|
122
|
+
adapterdata, util._monotonic_time())
|
123
|
+
if adapterdata:
|
124
|
+
anames = {}
|
125
|
+
for adata, _ in adapterdata:
|
126
|
+
skipadapter = False
|
127
|
+
clabel = adata['Slot']['Location']['PartLocation']['LocationType']
|
128
|
+
if clabel != 'Embedded':
|
129
|
+
aslot = adata['Slot']['Location']['PartLocation']['LocationOrdinalValue']
|
130
|
+
clabel = 'Slot {0}'.format(aslot)
|
131
|
+
aname = adata['Name']
|
132
|
+
bdata = {'location': clabel, 'name': aname}
|
133
|
+
if aname in anames:
|
134
|
+
anames[aname] += 1
|
135
|
+
aname = '{0} {1}'.format(aname, anames[aname])
|
136
|
+
else:
|
137
|
+
anames[aname] = 1
|
138
|
+
pcislot = adata['Id'].split('_')[1]
|
139
|
+
bdata['pcislot'] = '{0}:{1}.{2}'.format(
|
140
|
+
pcislot[:4].replace('0x',''), pcislot[4:6], pcislot[6:8]
|
141
|
+
)
|
142
|
+
serialdata = adata.get('SerialNumber', '')
|
143
|
+
if (serialdata and serialdata != 'N/A'
|
144
|
+
and '---' not in serialdata):
|
145
|
+
bdata['serial'] = serialdata
|
146
|
+
partnum = adata.get('PartNumber', '')
|
147
|
+
if partnum and partnum != 'N/A':
|
148
|
+
bdata['Part Number'] = partnum
|
149
|
+
fundata = self._get_expanded_data(adata['PCIeFunctions']['@odata.id'])
|
150
|
+
venid = fundata['Members'][0].get('VendorId', None)
|
151
|
+
if venid is not None:
|
152
|
+
bdata['PCI Vendor ID'] = venid.lower().split('0x')[-1]
|
153
|
+
devid = fundata['Members'][0].get('DeviceId', None)
|
154
|
+
if devid is not None and 'PCIE Device ID' not in bdata:
|
155
|
+
bdata['PCI Device ID'] = devid.lower().split('0x')[-1]
|
156
|
+
subvenid = fundata['Members'][0].get('SubsystemVendorId', None)
|
157
|
+
if subvenid is not None:
|
158
|
+
bdata['PCI Subsystem Vendor ID'] = subvenid.lower().split('0x')[-1]
|
159
|
+
subdevid = fundata['Members'][0].get('SubsystemId', None)
|
160
|
+
if subdevid is not None:
|
161
|
+
bdata['PCI Subsystem Device ID'] = subdevid.lower().split('0x')[-1]
|
162
|
+
bdata['FRU Number'] = adata.get('SKU', '')
|
163
|
+
|
164
|
+
# Could be identified also through Oem->Lenovo->FunctionClass
|
165
|
+
if fundata['Members'][0]['DeviceClass'] == 'NetworkController':
|
166
|
+
ports_data = self._get_expanded_data('{0}/Ports'.format(adata['@odata.id'].replace('PCIeDevices','NetworkAdapters')))
|
167
|
+
for port in ports_data['Members']:
|
168
|
+
bdata['MAC Address {0}'.format(port['Id'])] = port['Ethernet']['AssociatedMACAddresses'][0].lower()
|
169
|
+
hwmap[aname] = bdata
|
170
|
+
self.datacache['lenovo_cached_hwmap'] = (hwmap,
|
171
|
+
util._monotonic_time())
|
172
|
+
# self.weblogout()
|
173
|
+
return hwmap
|
174
|
+
|
175
|
+
def get_disk_firmware(self, diskent, prefix=''):
|
176
|
+
bdata = {}
|
177
|
+
if not prefix:
|
178
|
+
location = diskent.get('Name', '')
|
179
|
+
if location.startswith('M.2'):
|
180
|
+
prefix = 'M.2-'
|
181
|
+
elif location.startswith('7MM'):
|
182
|
+
prefix = '7MM-'
|
183
|
+
diskname = 'Disk {0}{1}'.format(prefix, diskent['PhysicalLocation']['PartLocation']['LocationOrdinalValue'])
|
184
|
+
bdata['model'] = '{0}_{1}'.format(diskent['Manufacturer'].rstrip(), diskent['Model'].rstrip())
|
185
|
+
bdata['version'] = diskent.get('FirmwareVersion','')
|
186
|
+
return (diskname, bdata)
|
187
|
+
|
188
|
+
def get_disk_hardware(self, diskent, prefix=''):
|
189
|
+
bdata = {}
|
190
|
+
if not prefix:
|
191
|
+
location = diskent.get('Name', '')
|
192
|
+
if location.startswith('M.2'):
|
193
|
+
prefix = 'M.2-'
|
194
|
+
elif location.startswith('7MM'):
|
195
|
+
prefix = '7MM-'
|
196
|
+
diskname = 'Disk {0}{1}'.format(prefix, diskent['PhysicalLocation']['PartLocation']['LocationOrdinalValue'])
|
197
|
+
bdata['Model'] = '{0}_{1}'.format(diskent['Manufacturer'].rstrip(), diskent['Model'].rstrip())
|
198
|
+
bdata['Serial Number'] = diskent['SerialNumber'].rstrip()
|
199
|
+
bdata['FRU Number'] = diskent['SKU'].rstrip()
|
200
|
+
bdata['Description'] = diskent['Oem']['Lenovo']['TypeString'].rstrip()
|
201
|
+
return (diskname, bdata)
|
202
|
+
|
203
|
+
def disk_inventory(self, mode=0):
|
204
|
+
# mode 0 is firmware, 1 is hardware
|
205
|
+
storagedata = self.get_cached_data('lenovo_cached_storage')
|
206
|
+
if not storagedata:
|
207
|
+
if self.webclient:
|
208
|
+
storagedata = self._do_bulk_requests([i['@odata.id'] for i in self.webclient.grab_json_response(
|
209
|
+
'/redfish/v1/Chassis/1')['Links']['Drives']])
|
210
|
+
if storagedata:
|
211
|
+
self.datacache['lenovo_cached_storage'] = (
|
212
|
+
storagedata, util._monotonic_time())
|
213
|
+
# Unmanaged disks cannot be retrieved through Redfish API
|
214
|
+
if storagedata:
|
215
|
+
for diskent, _ in storagedata:
|
216
|
+
if mode == 0:
|
217
|
+
yield self.get_disk_firmware(diskent)
|
218
|
+
elif mode == 1:
|
219
|
+
yield self.get_disk_hardware(diskent)
|
220
|
+
|
221
|
+
def get_storage_configuration(self, logout=True):
|
222
|
+
rsp = self._get_expanded_data("/redfish/v1/Systems/1/Storage")
|
223
|
+
standalonedisks = []
|
224
|
+
pools = []
|
225
|
+
for item in rsp.get('Members',[]):
|
226
|
+
cdisks = [item['Drives'][i]['@odata.id'] for i in range(len(item['Drives']))]
|
227
|
+
cid = '{0},{1}'.format(
|
228
|
+
item['Id'],
|
229
|
+
item['StorageControllers'][0]['Location']['PartLocation'].get('LocationOrdinalValue', -1))
|
230
|
+
storage_pools = self._get_expanded_data(item['StoragePools']['@odata.id'])
|
231
|
+
for p in storage_pools['Members']:
|
232
|
+
vols = self._get_expanded_data(p['AllocatedVolumes']['@odata.id'])
|
233
|
+
for vol in vols['Members']:
|
234
|
+
volumes=[]
|
235
|
+
disks=[]
|
236
|
+
spares=[]
|
237
|
+
volumes.append(
|
238
|
+
storage.Volume(name=vol['DisplayName'],
|
239
|
+
size=int(vol['CapacityBytes'])/1024//1024,
|
240
|
+
status=vol['Status']['Health'],
|
241
|
+
id=(cid,vol['Id'])))
|
242
|
+
for item_key, disk_ids in vol['Links'].items():
|
243
|
+
if isinstance(disk_ids, list) and 'drives' in item_key.lower():
|
244
|
+
for disk in disk_ids:
|
245
|
+
if disk['@odata.id'] in cdisks:
|
246
|
+
cdisks.remove(disk['@odata.id'])
|
247
|
+
disk_data = self.webclient.grab_json_response(disk['@odata.id'])
|
248
|
+
(spares if disk_data['Oem']['Lenovo']['DriveStatus']=="DedicatedHotspare" else disks).append(
|
249
|
+
storage.Disk(
|
250
|
+
name=disk_data['Name'], description=disk_data['Oem']['Lenovo']['TypeString'],
|
251
|
+
id=(cid, disk_data['Id']), status=disk_data['Oem']['Lenovo']['DriveStatus'],
|
252
|
+
serial=disk_data['SerialNumber'], fru=disk_data['SKU']))
|
253
|
+
raid=vol['RAIDType']
|
254
|
+
totalsize = int(p['Capacity']['Data']['AllocatedBytes'])/1024//1024
|
255
|
+
freesize = totalsize - int(p['Capacity']['Data']['ConsumedBytes'])/1024//1024
|
256
|
+
pools.append(storage.Array(
|
257
|
+
disks=disks, raid=raid, volumes=volumes,
|
258
|
+
id=(cid, p['Id']), hotspares=spares,
|
259
|
+
capacity=totalsize, available_capacity=freesize))
|
260
|
+
for d in cdisks:
|
261
|
+
disk_data = self.webclient.grab_json_response(d)
|
262
|
+
standalonedisks.append(
|
263
|
+
storage.Disk(
|
264
|
+
name=disk_data['Name'], description=disk_data['Oem']['Lenovo']['TypeString'],
|
265
|
+
id=(cid, disk_data['Id']), status=disk_data['Oem']['Lenovo']['DriveStatus'],
|
266
|
+
serial=disk_data['SerialNumber'], fru=disk_data['SKU']))
|
267
|
+
return storage.ConfigSpec(disks=standalonedisks, arrays=pools)
|
268
|
+
|
269
|
+
def check_storage_configuration(self, cfgspec=None):
|
270
|
+
rsp = self.webclient.grab_json_response(
|
271
|
+
'/api/providers/raidlink_GetStatus')
|
272
|
+
if rsp['return'] != 0 or rsp['status'] != 1:
|
273
|
+
raise pygexc.TemporaryError('Storage configuration unavailable in '
|
274
|
+
'current state (try boot to setup or '
|
275
|
+
'an OS)')
|
276
|
+
return True
|
277
|
+
|
278
|
+
def apply_storage_configuration(self, cfgspec):
|
279
|
+
realcfg = self.get_storage_configuration(False)
|
280
|
+
for disk in cfgspec.disks:
|
281
|
+
if disk.status.lower() == 'jbod':
|
282
|
+
self._make_jbod(disk, realcfg)
|
283
|
+
elif disk.status.lower() == 'hotspare':
|
284
|
+
self._make_global_hotspare(disk, realcfg)
|
285
|
+
elif disk.status.lower() in ('unconfigured', 'available', 'ugood',
|
286
|
+
'unconfigured good'):
|
287
|
+
self._make_available(disk, realcfg)
|
288
|
+
for pool in cfgspec.arrays:
|
289
|
+
if pool.disks:
|
290
|
+
self._create_array(pool)
|
291
|
+
|
292
|
+
def _make_available(self, disk, realcfg):
|
293
|
+
currstatus = self._get_status(disk, realcfg)
|
294
|
+
newstate = None
|
295
|
+
if currstatus.lower() == 'unconfiguredgood':
|
296
|
+
return
|
297
|
+
elif currstatus.lower() == 'globalhotspare':
|
298
|
+
newstate = "None"
|
299
|
+
elif currstatus.lower() == 'jbod':
|
300
|
+
newstate = "MakeUnconfiguredGood"
|
301
|
+
self._set_drive_state(disk, newstate)
|
302
|
+
|
303
|
+
def _make_jbod(self, disk, realcfg):
|
304
|
+
currstatus = self._get_status(disk, realcfg)
|
305
|
+
if currstatus.lower() == 'jbod':
|
306
|
+
return
|
307
|
+
self._make_available(disk, realcfg)
|
308
|
+
self._set_drive_state(disk, "MakeJBOD")
|
309
|
+
|
310
|
+
def _make_global_hotspare(self, disk, realcfg):
|
311
|
+
currstatus = self._get_status(disk, realcfg)
|
312
|
+
if currstatus.lower() == 'globalhotspare':
|
313
|
+
return
|
314
|
+
self._make_available(disk, realcfg)
|
315
|
+
self._set_drive_state(disk, "Global")
|
316
|
+
|
317
|
+
def _set_drive_state(self, disk, state):
|
318
|
+
raid_alldevices = self.webclient.grab_json_response(
|
319
|
+
'/api/providers/raid_alldevices')
|
320
|
+
if raid_alldevices.get('return', -1) != 0:
|
321
|
+
raise Exception(
|
322
|
+
'Unexpected return to get all RAID devices information')
|
323
|
+
for c in raid_alldevices.get('StorageComplexes',[]):
|
324
|
+
cslot = str(c.get('SlotNumber'))
|
325
|
+
if cslot == disk.id[0].split(',')[1]:
|
326
|
+
c_pciaddr = c.get('PCIeAddress',-1)
|
327
|
+
cdrives = c.get('Drives',[])
|
328
|
+
for d in cdrives:
|
329
|
+
if disk.id[1] == d.get('Id',''):
|
330
|
+
currstatus = d['Oem']['Lenovo']['DriveStatus']
|
331
|
+
d_resid = d['Internal']['ResourceId']
|
332
|
+
if state in ("Global", "None"):
|
333
|
+
data = {
|
334
|
+
"controller_address": c_pciaddr,
|
335
|
+
"drive_resource_id": d_resid,
|
336
|
+
"hotspare_type": state,
|
337
|
+
"pool_resource_ids": []}
|
338
|
+
raidlink_url = '/api/providers/raidlink_AssignHotSpare'
|
339
|
+
else:
|
340
|
+
data = {
|
341
|
+
"controller_address": c_pciaddr,
|
342
|
+
"drive_operation": state,
|
343
|
+
"drive_resource_ids": [d_resid]}
|
344
|
+
raidlink_url = '/api/providers/raidlink_DiskStateAction'
|
345
|
+
msg = self._do_web_request(raidlink_url, method='POST',
|
346
|
+
payload=data, cache=False)
|
347
|
+
if msg.get('return', -1) != 0:
|
348
|
+
raise Exception(
|
349
|
+
'Unexpected return to set disk state: {0}'.format(
|
350
|
+
msg.get('return', -1)))
|
351
|
+
set_state_token = msg.get('token', '')
|
352
|
+
msg = self._do_web_request(
|
353
|
+
'/api/providers/raidlink_QueryAsyncStatus',
|
354
|
+
method='POST',
|
355
|
+
payload={"token": set_state_token},
|
356
|
+
cache=False)
|
357
|
+
while msg['status'] == 2:
|
358
|
+
time.sleep(1)
|
359
|
+
msg = self._do_web_request(
|
360
|
+
'/api/providers/raidlink_QueryAsyncStatus',
|
361
|
+
method='POST',
|
362
|
+
payload={"token": set_state_token},
|
363
|
+
cache=False)
|
364
|
+
if msg.get('return',-1) != 0 or msg.get('status',-1) != 0:
|
365
|
+
raise Exception(
|
366
|
+
'Unexpected return to set disk state: {0}'.format(
|
367
|
+
msg.get('return', -1)))
|
368
|
+
disk_url=f"/redfish/v1/Systems/1/Storage/{disk.id[0].split(',')[0]}/Drives/{disk.id[1]}"
|
369
|
+
newstatus = self.webclient.grab_json_response(disk_url)
|
370
|
+
disk_converted = False
|
371
|
+
for _ in range(60):
|
372
|
+
if currstatus == newstatus['Oem']['Lenovo']['DriveStatus']:
|
373
|
+
time.sleep(1)
|
374
|
+
newstatus = self.webclient.grab_json_response(disk_url)
|
375
|
+
else:
|
376
|
+
disk_converted = True
|
377
|
+
break
|
378
|
+
if not disk_converted:
|
379
|
+
raise Exception(
|
380
|
+
'Disk set command was successful, but the disk state is unchanged')
|
381
|
+
|
382
|
+
def _get_status(self, disk, realcfg):
|
383
|
+
for cfgdisk in realcfg.disks:
|
384
|
+
if disk.id == cfgdisk.id:
|
385
|
+
currstatus = cfgdisk.status
|
386
|
+
break
|
387
|
+
else:
|
388
|
+
raise pygexc.InvalidParameterValue('Requested disk not found')
|
389
|
+
return currstatus
|
390
|
+
|
391
|
+
def remove_storage_configuration(self, cfgspec):
|
392
|
+
realcfg = self.get_storage_configuration(False)
|
393
|
+
for pool in cfgspec.arrays:
|
394
|
+
for volume in pool.volumes:
|
395
|
+
cid = volume.id[0].split(',')[0]
|
396
|
+
vid = volume.id[1]
|
397
|
+
msg, code = self.webclient.grab_json_response_with_status(
|
398
|
+
f'/redfish/v1/Systems/1/Storage/{cid}/Volumes/{vid}',
|
399
|
+
method='DELETE')
|
400
|
+
if code == 500:
|
401
|
+
raise Exception(
|
402
|
+
'Unexpected return to volume deletion: ' + repr(msg))
|
403
|
+
for disk in cfgspec.disks:
|
404
|
+
self._make_available(disk, realcfg)
|
405
|
+
|
406
|
+
def _parse_array_spec(self, arrayspec):
|
407
|
+
controller = None
|
408
|
+
if arrayspec.disks:
|
409
|
+
for disk in list(arrayspec.disks) + list(arrayspec.hotspares):
|
410
|
+
if controller is None:
|
411
|
+
controller = disk.id[0]
|
412
|
+
if controller != disk.id[0]:
|
413
|
+
raise pygexc.UnsupportedFunctionality(
|
414
|
+
'Cannot span arrays across controllers')
|
415
|
+
raidmap = self._raid_number_map(controller)
|
416
|
+
if not raidmap:
|
417
|
+
raise pygexc.InvalidParameterValue(
|
418
|
+
'No RAID Type supported on this controller')
|
419
|
+
requestedlevel = str(arrayspec.raid)
|
420
|
+
if requestedlevel not in raidmap:
|
421
|
+
raise pygexc.InvalidParameterValue(
|
422
|
+
'Requested RAID Type "{0}" not available on this '
|
423
|
+
'controller. Allowed values are: {1}'.format(
|
424
|
+
requestedlevel, [k for k in raidmap]))
|
425
|
+
rdinfo = raidmap[str(arrayspec.raid).lower()]
|
426
|
+
rdlvl = str(rdinfo[0])
|
427
|
+
defspan = 1 if rdinfo[1] == 1 else 2
|
428
|
+
spancount = defspan if arrayspec.spans is None else arrayspec.spans
|
429
|
+
drivesperspan = str(len(arrayspec.disks) // int(spancount))
|
430
|
+
hotspares = arrayspec.hotspares
|
431
|
+
drives = arrayspec.disks
|
432
|
+
minimal_conditions = {
|
433
|
+
"RAID0": (1,128,1),
|
434
|
+
"RAID1": (2,2,1),
|
435
|
+
"RAID1Triple": (3,3,1),
|
436
|
+
"RAID10": (4,128,2),
|
437
|
+
"RAID10Triple": (6,128,3),
|
438
|
+
"RAID5": (3,128,1),
|
439
|
+
"RAID50": (6,128,2),
|
440
|
+
"RAID6": (4,128,1),
|
441
|
+
"RAID60": (8, 128, 2)
|
442
|
+
}
|
443
|
+
raid_level = rdinfo[0]
|
444
|
+
min_pd = minimal_conditions[raid_level][0]
|
445
|
+
max_pd = minimal_conditions[raid_level][1]
|
446
|
+
disk_multiplier = minimal_conditions[raid_level][2]
|
447
|
+
if len(drives) < min_pd or \
|
448
|
+
len(drives) > max_pd or \
|
449
|
+
len(drives) % disk_multiplier != 0:
|
450
|
+
raise pygexc.InvalidParameterValue(
|
451
|
+
f'Number of disks for {rdinfo} must be between {min_pd} and {max_pd} and be a multiple of {disk_multiplier}')
|
452
|
+
if hotspares:
|
453
|
+
hstr = '|'.join([str(x.id[1]) for x in hotspares]) + '|'
|
454
|
+
else:
|
455
|
+
hstr = ''
|
456
|
+
drvstr = '|'.join([str(x.id[1]) for x in drives]) + '|'
|
457
|
+
return {
|
458
|
+
'controller': controller,
|
459
|
+
'drives': drvstr,
|
460
|
+
'hotspares': hstr,
|
461
|
+
'raidlevel': rdlvl,
|
462
|
+
'spans': spancount,
|
463
|
+
'perspan': drivesperspan,
|
464
|
+
}
|
465
|
+
else:
|
466
|
+
# TODO(Jarrod Johnson): adding new volume to
|
467
|
+
# existing array would be here
|
468
|
+
pass
|
469
|
+
|
470
|
+
def _raid_number_map(self, controller):
|
471
|
+
themap = {}
|
472
|
+
cid = controller.split(',')
|
473
|
+
rsp = self.webclient.grab_json_response(
|
474
|
+
'/redfish/v1/Systems/1/Storage/{0}'.format(cid[0]))
|
475
|
+
for rt in rsp['StorageControllers'][0]['SupportedRAIDTypes']:
|
476
|
+
rt_lower = rt.lower()
|
477
|
+
mapdata = (rt, 1)
|
478
|
+
themap[rt]=mapdata
|
479
|
+
themap[rt_lower] = mapdata
|
480
|
+
themap[rt_lower.replace('raid','r')] = mapdata
|
481
|
+
themap[rt_lower.replace('raid','')] = mapdata
|
482
|
+
return themap
|
483
|
+
|
484
|
+
def _create_array(self, pool):
|
485
|
+
params = self._parse_array_spec(pool)
|
486
|
+
cid = params['controller'].split(',')[0]
|
487
|
+
c_capabilities, code = self.webclient.grab_json_response_with_status(
|
488
|
+
f'/redfish/v1/Systems/1/Storage/{cid}/Volumes/Capabilities')
|
489
|
+
if code == 404:
|
490
|
+
c_capabilities, code = self.webclient.grab_json_response_with_status(
|
491
|
+
f'/redfish/v1/Systems/1/Storage/{cid}/Volumes/Oem/Lenovo/Capabilities')
|
492
|
+
if code == 404:
|
493
|
+
# If none of the endpoints exist, maybe it should be printed that
|
494
|
+
# no capabilities found, therefore default values will be used
|
495
|
+
# whatever they are
|
496
|
+
pass
|
497
|
+
volumes = pool.volumes
|
498
|
+
drives = [d for d in params['drives'].split("|") if d != '']
|
499
|
+
hotspares = [h for h in params['hotspares'].split("|") if h != '']
|
500
|
+
raidlevel = params['raidlevel']
|
501
|
+
nameappend = 1
|
502
|
+
currvolnames = None
|
503
|
+
currcfg = self.get_storage_configuration(False)
|
504
|
+
for vol in volumes:
|
505
|
+
if vol.name is None:
|
506
|
+
# need to iterate while there exists a volume of that name
|
507
|
+
if currvolnames is None:
|
508
|
+
currvolnames = set([])
|
509
|
+
for pool in currcfg.arrays:
|
510
|
+
for volume in pool.volumes:
|
511
|
+
currvolnames.add(volume.name)
|
512
|
+
name = 'Volume_{0}'.format(nameappend)
|
513
|
+
nameappend += 1
|
514
|
+
while name in currvolnames:
|
515
|
+
name = 'Volume_{0}'.format(nameappend)
|
516
|
+
nameappend += 1
|
517
|
+
else:
|
518
|
+
name = vol.name
|
519
|
+
|
520
|
+
# Won't check against Redfish allowable values as they not trustworthy yet
|
521
|
+
# Some values show in Redfish, but may not be accepted by UEFI/controller or vice versa
|
522
|
+
stripsize_map = {
|
523
|
+
'4': 4096, '4096': 4096,
|
524
|
+
'16': 16384, '16384': 16384,
|
525
|
+
'32': 32768, '32768': 32768,
|
526
|
+
'64': 65536, '65536': 65536,
|
527
|
+
'128': 131072, '131072': 131072,
|
528
|
+
'256': 262144, '262144': 262144,
|
529
|
+
'512': 524288, '524288': 524288,
|
530
|
+
'1024': 1048576, '1048576': 1048576
|
531
|
+
}
|
532
|
+
stripsize = stripsize_map[str(vol.stripsize).lower().replace('k','')] if vol.stripsize is not None else None
|
533
|
+
|
534
|
+
readpolicy_map = {'0': 'Off', '1': 'ReadAhead'}
|
535
|
+
read_policy = None
|
536
|
+
read_cache_possible = c_capabilities.get("ReadCachePolicy@Redfish.AllowableValues",[])
|
537
|
+
if read_cache_possible:
|
538
|
+
if vol.read_policy is not None:
|
539
|
+
if str(vol.read_policy) in readpolicy_map:
|
540
|
+
vol.read_policy = readpolicy_map[str(vol.read_policy)]
|
541
|
+
if vol.read_policy in read_cache_possible:
|
542
|
+
read_policy = vol.read_policy
|
543
|
+
else:
|
544
|
+
raise pygexc.InvalidParameterValue(
|
545
|
+
f'{vol.read_policy} Read Cache Policy is not supported. Allowed values are: {read_cache_possible}')
|
546
|
+
|
547
|
+
writepolicy_map = {'0': 'WriteThrough', '1': 'UnprotectedWriteBack',
|
548
|
+
'2': 'ProtectedWriteBack', '3': 'Off'}
|
549
|
+
write_policy = None
|
550
|
+
write_cache_possible = c_capabilities.get("WriteCachePolicy@Redfish.AllowableValues",[])
|
551
|
+
if write_cache_possible:
|
552
|
+
if vol.write_policy is not None:
|
553
|
+
if str(vol.write_policy) in writepolicy_map:
|
554
|
+
vol.write_policy = writepolicy_map[str(vol.write_policy)]
|
555
|
+
if vol.write_policy in write_cache_possible:
|
556
|
+
write_policy = vol.write_policy
|
557
|
+
else:
|
558
|
+
raise pygexc.InvalidParameterValue(
|
559
|
+
f'{vol.write_policy} Write Cache Policy is not supported. Allowed values are: {write_cache_possible}')
|
560
|
+
|
561
|
+
defaultinit_map = {'0': 'No', '1': 'Fast', '2': 'Full'}
|
562
|
+
default_init = None
|
563
|
+
default_init_possible = c_capabilities.get("InitializationType@Redfish.AllowableValues",[])
|
564
|
+
if default_init_possible:
|
565
|
+
if vol.default_init is not None:
|
566
|
+
if str(vol.default_init) in defaultinit_map:
|
567
|
+
vol.default_init = defaultinit_map[str(vol.default_init)]
|
568
|
+
if vol.default_init in default_init_possible:
|
569
|
+
default_init = vol.default_init
|
570
|
+
else:
|
571
|
+
raise pygexc.InvalidParameterValue(
|
572
|
+
f'{vol.default_init} Initialization Type is not supported. Allowed values are: {default_init_possible}')
|
573
|
+
|
574
|
+
volsize = None
|
575
|
+
spec_disks = sorted(drives)
|
576
|
+
spec_hotspares = hotspares
|
577
|
+
for array in currcfg.arrays:
|
578
|
+
in_use_disks = sorted([d.id[1] for d in array.disks])
|
579
|
+
in_use_hotspares = [h.id[1] for h in array.hotspares]
|
580
|
+
if spec_disks == in_use_disks:
|
581
|
+
if vol.size is None:
|
582
|
+
volsize = array.available_capacity
|
583
|
+
array.available_capacity = 0
|
584
|
+
break
|
585
|
+
else:
|
586
|
+
strsize = str(vol.size)
|
587
|
+
if strsize in ('all','100%'):
|
588
|
+
raise pygexc.InvalidParameterValue(
|
589
|
+
f'Requested size for volume {name} exceeds available capacity. Available capacity is {array.available_capacity} MiB')
|
590
|
+
elif strsize.endswith('%'):
|
591
|
+
volsize = int(array.capacity
|
592
|
+
* float(strsize.replace('%', ''))
|
593
|
+
/ 100.0)
|
594
|
+
if volsize > array.available_capacity:
|
595
|
+
raise pygexc.InvalidParameterValue(
|
596
|
+
f'Requested size for volume {name} exceeds available capacity. Available capacity is {array.available_capacity} MiB')
|
597
|
+
else:
|
598
|
+
array.available_capacity-=volsize
|
599
|
+
else:
|
600
|
+
try:
|
601
|
+
volsize = int(strsize)
|
602
|
+
if volsize > array.available_capacity:
|
603
|
+
raise pygexc.InvalidParameterValue(
|
604
|
+
f'Requested size for volume {name} exceeds available capacity. Available capacity is {array.available_capacity} MiB')
|
605
|
+
else:
|
606
|
+
array.available_capacity-=volsize
|
607
|
+
except ValueError:
|
608
|
+
raise pygexc.InvalidParameterValue(
|
609
|
+
'Unrecognized size ' + strsize)
|
610
|
+
elif any(d in in_use_disks for d in spec_disks):
|
611
|
+
raise pygexc.InvalidParameterValue(
|
612
|
+
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}')
|
613
|
+
else:
|
614
|
+
disks_capacities = {}
|
615
|
+
for d in spec_disks:
|
616
|
+
disks_capacities[d] = self.webclient.grab_json_response(
|
617
|
+
f'/redfish/v1/Systems/1/Storage/{cid}/Drives/{d}')["CapacityBytes"]
|
618
|
+
max_capacity = sum(v for k,v in disks_capacities.items())
|
619
|
+
min_disk = min([v for k,v in disks_capacities.items()])
|
620
|
+
disk_count = len(disks_capacities)
|
621
|
+
max_capacity_per_raid = {
|
622
|
+
"RAID0": max_capacity,
|
623
|
+
"RAID1": min_disk,
|
624
|
+
"RAID1Triple": min_disk,
|
625
|
+
"RAID10": (disk_count//2)*min_disk,
|
626
|
+
"RAID10Triple": (disk_count//3)*min_disk,
|
627
|
+
"RAID5": (disk_count-1)*min_disk,
|
628
|
+
"RAID50": (disk_count-2)*min_disk,
|
629
|
+
"RAID6": (disk_count-2)*min_disk,
|
630
|
+
"RAID60": (disk_count-4)*min_disk
|
631
|
+
}
|
632
|
+
if vol.size is not None:
|
633
|
+
strsize = str(vol.size)
|
634
|
+
if strsize.endswith('%'):
|
635
|
+
volsize = int(max_capacity_per_raid[raidlevel]
|
636
|
+
* float(strsize.replace('%', ''))
|
637
|
+
/ 100.0)
|
638
|
+
else:
|
639
|
+
try:
|
640
|
+
volsize = int(strsize)
|
641
|
+
if volsize > max_capacity_per_raid[raidlevel]:
|
642
|
+
raise pygexc.InvalidParameterValue(
|
643
|
+
f'Requested size for volume {name} exceeds available capacity. Available capacity is {max_capacity_per_raid[raidlevel]} bytes')
|
644
|
+
except ValueError:
|
645
|
+
raise pygexc.InvalidParameterValue(
|
646
|
+
'Unrecognized size ' + strsize)
|
647
|
+
for h in spec_hotspares:
|
648
|
+
if h in in_use_hotspares:
|
649
|
+
raise pygexc.InvalidParameterValue(
|
650
|
+
f'Hotspare {h} from provided config is in use by another volume.')
|
651
|
+
|
652
|
+
request_data = {
|
653
|
+
"Name":name,
|
654
|
+
"RAIDType":raidlevel,
|
655
|
+
"Links":{
|
656
|
+
"Drives":[
|
657
|
+
{'@odata.id': f'/redfish/v1/Systems/1/Storage/{cid}/Drives/{did}'} for did in spec_disks]}}
|
658
|
+
if spec_hotspares:
|
659
|
+
request_data["Links"]["DedicatedSpareDrives"] = {[
|
660
|
+
{'@odata.id': f'/redfish/v1/Systems/1/Storage/{cid}/Drives/{hid}' for hid in spec_hotspares}]}
|
661
|
+
if volsize:
|
662
|
+
request_data["CapacityBytes"] = volsize
|
663
|
+
if stripsize:
|
664
|
+
request_data["StripSizeBytes"] = stripsize
|
665
|
+
if read_policy:
|
666
|
+
request_data["ReadCachePolicy"] = read_policy
|
667
|
+
if write_policy:
|
668
|
+
request_data["WriteCachePolicy"] = write_policy
|
669
|
+
|
670
|
+
msg, code=self.webclient.grab_json_response_with_status(
|
671
|
+
f'/redfish/v1/Systems/1/Storage/{cid}/Volumes',
|
672
|
+
method='POST',
|
673
|
+
data=request_data)
|
674
|
+
if code == 500:
|
675
|
+
raise Exception("Unexpected response to volume creation: " + repr(msg))
|
676
|
+
time.sleep(60)
|
677
|
+
#Even if in web the volume appears immediately, get_storage_configuration does not see it that fast
|
678
|
+
newcfg = self.get_storage_configuration(False)
|
679
|
+
newvols = [v.id for p in newcfg.arrays for v in p.volumes]
|
680
|
+
currvols = [v.id for p in currcfg.arrays for v in p.volumes]
|
681
|
+
newvol = list(set(newvols) - set(currvols))[0]
|
682
|
+
if default_init:
|
683
|
+
msg, code = self.webclient.grab_json_response_with_status(
|
684
|
+
f'/redfish/v1/Systems/1/Storage/{cid}/Volumes/{newvol[1]}/Actions/Volume.Initialize',
|
685
|
+
method='POST',
|
686
|
+
data = {"InitializeType": default_init})
|
687
|
+
if code == 500:
|
688
|
+
raise Exception("Unexpected response to volume initialization: " + repr(msg))
|
689
|
+
|
690
|
+
def attach_remote_media(self, url, user, password, vmurls):
|
691
|
+
for vmurl in vmurls:
|
692
|
+
if 'EXT' not in vmurl:
|
693
|
+
continue
|
694
|
+
vminfo = self._do_web_request(vmurl, cache=False)
|
695
|
+
if vminfo['ConnectedVia'] != 'NotConnected':
|
696
|
+
continue
|
697
|
+
msg,code = self.webclient.grab_json_response_with_status(
|
698
|
+
vmurl,
|
699
|
+
data={'Image': url, 'Inserted': True},
|
700
|
+
method='PATCH')
|
701
|
+
if code == 500:
|
702
|
+
raise Exception("Unexpected response when attaching remote media: " + repr(msg))
|
703
|
+
raise pygexc.BypassGenericBehavior()
|
704
|
+
break
|
705
|
+
else:
|
706
|
+
raise pygexc.InvalidParameterValue(
|
707
|
+
'XCC does not have required license for operation')
|
44
708
|
|
45
709
|
def get_screenshot(self, outfile):
|
46
710
|
wc = self.webclient.dupe()
|
@@ -480,5 +1144,3 @@ class OEMHandler(generic.OEMHandler):
|
|
480
1144
|
'Name': 'HPM-FPGA Pending',
|
481
1145
|
'build': pendinghpm}
|
482
1146
|
raise pygexc.BypassGenericBehavior()
|
483
|
-
|
484
|
-
|
@@ -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>
|
@@ -45,7 +45,7 @@ pyghmi/ipmi/private/util.py,sha256=ayYodiSydlrrt0_pQppoRB1T6n-KNOiHZSfAlCMcpG0,3
|
|
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=
|
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=
|
58
|
+
pyghmi/redfish/oem/lenovo/xcc3.py,sha256=j5zd1QAF5fokEFAsXtaMkIInjvwgSw-BcpNphPT1h-Q,54587
|
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
|
@@ -64,11 +64,11 @@ pyghmi/tests/unit/ipmi/test_sdr.py,sha256=vb3iLY0cnHJ2K_m4xgYUjEcbPd_ZYhYx-uBowB
|
|
64
64
|
pyghmi/util/__init__.py,sha256=GZLBWJiun2Plb_VE9dDSh4_PQMCha3gA7QLUqx3oSYI,25
|
65
65
|
pyghmi/util/parse.py,sha256=6VlyBCEcE8gy8PJWmEDdtCyWATaKwPaTswCdioPCWOE,2120
|
66
66
|
pyghmi/util/webclient.py,sha256=782_yMuy_LuN9E2vh2EJ-R64X_EyvLLRuurE__jfn20,15371
|
67
|
-
pyghmi-1.6.
|
68
|
-
pyghmi-1.6.
|
69
|
-
pyghmi-1.6.
|
70
|
-
pyghmi-1.6.
|
71
|
-
pyghmi-1.6.
|
72
|
-
pyghmi-1.6.
|
73
|
-
pyghmi-1.6.
|
74
|
-
pyghmi-1.6.
|
67
|
+
pyghmi-1.6.2.dist-info/AUTHORS,sha256=yv4aQom_PII-SNqbeeKrfH-spcG85PRPsZ71Iqjl_fU,2083
|
68
|
+
pyghmi-1.6.2.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
69
|
+
pyghmi-1.6.2.dist-info/METADATA,sha256=dFiGyB-IAQ3tFzBHsgS1NmQLn9mX82IJxFknP4Bmq8o,1136
|
70
|
+
pyghmi-1.6.2.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
|
71
|
+
pyghmi-1.6.2.dist-info/entry_points.txt,sha256=-OpJliDzATxmuPXK0VR3Ma-Yk_i4ZhfIIB-12A26dSI,168
|
72
|
+
pyghmi-1.6.2.dist-info/pbr.json,sha256=KQJXkpzIDiMZ-dqqjT_r8x1mofvwXWnVAUoUJc484J4,46
|
73
|
+
pyghmi-1.6.2.dist-info/top_level.txt,sha256=aDtt6S9eVu6-tNdaUs4Pz9PbdUd69bziZZMhNvk9Ulc,7
|
74
|
+
pyghmi-1.6.2.dist-info/RECORD,,
|
@@ -0,0 +1 @@
|
|
1
|
+
{"git_version": "6bf4e17", "is_release": true}
|
pyghmi-1.6.1.dist-info/pbr.json
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
{"git_version": "33cff21", "is_release": true}
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|