Implement support for ThinkSystem servers

Change-Id: Ic45943c4b882bb13390722cca58b8b1785386e86
This commit is contained in:
Jarrod Johnson 2017-06-20 13:23:39 -04:00
parent 4c366d2f47
commit d1f48b02c0
7 changed files with 281 additions and 54 deletions

0
buildrpm Normal file → Executable file
View File

View File

@ -1490,9 +1490,7 @@ class Command(object):
raise Exception('name must be less than or = 16 chars')
name = name.ljust(16, "\x00")
data.extend([ord(x) for x in name])
response = self.raw_command(netfn=0x06, command=0x45, data=data)
if 'error' in response:
raise Exception(response['error'])
self.xraw_command(netfn=0x06, command=0x45, data=data)
return True
def get_user_name(self, uid, return_none_on_error=True):
@ -1554,12 +1552,16 @@ class Command(object):
else:
password = password.ljust(16, "\x00")
data.extend([ord(x) for x in password])
response = self.raw_command(netfn=0x06, command=0x47, data=data)
if 'error' in response:
try:
self.xraw_command(netfn=0x06, command=0x47, data=data)
except exc.IpmiException as ie:
if mode == 'test_password':
# return false if password test failed
return False
raise Exception(response['error'])
elif mode in ('enable', 'disable') and ie.ipmicode == 0xcc:
# Some BMCs see redundant calls to password disable/enable
# as invalid
return True
raise
return True
def get_channel_max_user_count(self, channel=None):
@ -1689,7 +1691,9 @@ class Command(object):
try:
# First try to set name to all \x00 explicitly
self.set_user_name(uid, '')
except Exception:
except exc.IpmiException as ie:
if ie.ipmicode != 0xcc:
raise
# An invalid data field in request is frequently reported.
# however another convention that exists is all '\xff'
# if this fails, pass up the error so that calling code knows

View File

@ -143,12 +143,16 @@ class OEMHandler(generic.OEMHandler):
# will need to retain data to differentiate
# variations. For example System X versus Thinkserver
self.oemid = oemid
self._fpc_variant = None
self.ipmicmd = weakref.proxy(ipmicmd)
self._has_megarac = None
self.oem_inventory_info = None
self._mrethidx = None
self._hasimm = None
if self.has_imm:
self._hasxcc = None
if self.has_xcc:
self.immhandler = imm.XCCClient(ipmicmd)
elif self.has_imm:
self.immhandler = imm.IMMClient(ipmicmd)
@property
@ -281,9 +285,15 @@ class OEMHandler(generic.OEMHandler):
def is_fpc(self):
"""True if the target is a Lenovo nextscale fan power controller
"""
fpc_ids = ((19046, 32, 1063),)
return (self.oemid['manufacturer_id'], self.oemid['device_id'],
self.oemid['product_id']) in fpc_ids
fpc_id = (19046, 32, 1063)
smm_id = (19046, 32, 1180)
currid = (self.oemid['manufacturer_id'], self.oemid['device_id'],
self.oemid['product_id'])
if currid == fpc_id:
self._fpc_variant = 6
elif currid == smm_id:
self._fpc_variant = 2
return self._fpc_variant
@property
def is_sd350(self):
@ -327,17 +337,19 @@ class OEMHandler(generic.OEMHandler):
def get_sensor_data(self):
if self.is_fpc:
for name in nextscale.get_sensor_names():
yield nextscale.get_sensor_reading(name, self.ipmicmd)
for name in nextscale.get_sensor_names(self._fpc_variant):
yield nextscale.get_sensor_reading(name, self.ipmicmd,
self._fpc_variant)
def get_sensor_descriptions(self):
if self.is_fpc:
return nextscale.get_sensor_descriptions()
return nextscale.get_sensor_descriptions(self._fpc_variant)
return ()
def get_sensor_reading(self, sensorname):
if self.is_fpc:
return nextscale.get_sensor_reading(sensorname, self.ipmicmd)
return nextscale.get_sensor_reading(sensorname, self.ipmicmd,
self._fpc_variant)
return ()
def get_inventory_of_component(self, component):
@ -486,6 +498,30 @@ class OEMHandler(generic.OEMHandler):
fru['oem_parser'] = None
return fru
@property
def has_xcc(self):
if self._hasxcc is not None:
return self._hasxcc
try:
bdata = self.ipmicmd.xraw_command(netfn=0x3a, command=0xc1)
except pygexc.IpmiException:
self._hasxcc = False
self._hasimm = False
return False
if len(bdata['data'][:]) != 3:
self._hasimm = False
self._hasxcc = False
return False
rdata = bytearray(bdata['data'][:])
self._hasxcc = rdata[1] & 16 == 16
if self._hasxcc:
# For now, have imm calls go to xcc, since they are providing same
# interface. Longer term the hope is that all the Lenovo
# stuff will branch at init, and not have conditionals
# in all the functions
self._hasimm = self._hasxcc
return self._hasxcc
@property
def has_imm(self):
if self._hasimm is not None:
@ -499,7 +535,7 @@ class OEMHandler(generic.OEMHandler):
self._hasimm = False
return False
rdata = bytearray(bdata['data'][:])
self._hasimm = (rdata[1] & 1 == 1) or (rdata[1] & 8 == 8)
self._hasimm = (rdata[1] & 1 == 1) or (rdata[1] & 16 == 16)
return self._hasimm
def get_oem_firmware(self, bmcver):
@ -509,6 +545,9 @@ class OEMHandler(generic.OEMHandler):
return command["parser"](rsp["data"])
elif self.has_imm:
return self.immhandler.get_firmware_inventory(bmcver)
elif self.is_fpc:
return nextscale.get_fpc_firmware(bmcver, self.ipmicmd,
self._fpc_variant)
return super(OEMHandler, self).get_oem_firmware(bmcver)
def get_oem_capping_enabled(self):

View File

@ -16,13 +16,15 @@
from datetime import datetime
import json
from pyghmi.ipmi.private.util import _monotonic_time
import pyghmi.ipmi.private.util as util
import pyghmi.util.webclient as webclient
import urllib
import weakref
class IMMClient(object):
logouturl = '/data/logout'
bmcname = 'IMM'
def __init__(self, ipmicmd):
self.ipmicmd = weakref.proxy(ipmicmd)
@ -50,6 +52,10 @@ class IMMClient(object):
return datetime.strptime(strval, '%m/%d/%Y')
except ValueError:
pass
try:
return datetime.strptime(strval, '%Y-%m-%d')
except ValueError:
pass
try:
return datetime.strptime(strval, '%m %d %Y')
except ValueError:
@ -133,7 +139,7 @@ class IMMClient(object):
def get_cached_data(self, attribute):
try:
kv = self.datacache[attribute]
if kv[1] > _monotonic_time() - 30:
if kv[1] > util._monotonic_time() - 30:
return kv[0]
except KeyError:
return None
@ -147,7 +153,7 @@ class IMMClient(object):
result = self.wc.grab_json_response('/data?set', params)
if result['return'] != 'Success':
raise Exception(result['reason'])
self.wc.grab_json_response('/data/logout')
self.weblogout()
def detach_remote_media(self):
mnt = self.wc.grab_json_response(
@ -165,7 +171,7 @@ class IMMClient(object):
result = self.wc.grab_json_response('/data?set', params)
if result['return'] != 'Success':
raise Exception(result['reason'])
self.wc.grab_json_response('/data/logout')
self.weblogout()
def fetch_agentless_firmware(self):
adapterdata = self.get_cached_data('lenovo_cached_adapters')
@ -175,7 +181,7 @@ class IMMClient(object):
'/designs/imm/dataproviders/imm_adapters.php')
if adapterdata:
self.datacache['lenovo_cached_adapters'] = (
adapterdata, _monotonic_time())
adapterdata, util._monotonic_time())
if adapterdata and 'items' in adapterdata:
for adata in adapterdata['items']:
aname = adata['adapter.adapterName']
@ -209,7 +215,7 @@ class IMMClient(object):
'/designs/imm/dataproviders/raid_alldevices.php')
if storagedata:
self.datacache['lenovo_cached_storage'] = (
storagedata, _monotonic_time())
storagedata, util._monotonic_time())
if storagedata and 'items' in storagedata:
for adp in storagedata['items']:
if 'storage.vpd.productName' not in adp:
@ -227,9 +233,7 @@ class IMMClient(object):
bdata['version'] = diskent['storage.firmwares'][0][
'versionStr']
yield (diskname, bdata)
if self.wc:
self.wc.grab_json_response('/data/logout')
self._wc = None
self.weblogout()
def get_hw_inventory(self):
hwmap = self.hardware_inventory_map()
@ -250,7 +254,7 @@ class IMMClient(object):
def weblogout(self):
if self._wc:
self._wc.grab_json_response('/data/logout')
self._wc.grab_json_response(self.logouturl)
self._wc = None
def hardware_inventory_map(self):
@ -265,7 +269,7 @@ class IMMClient(object):
'/designs/imm/dataproviders/imm_adapters.php')
if adapterdata:
self.datacache['lenovo_cached_adapters'] = (
adapterdata, _monotonic_time())
adapterdata, util._monotonic_time())
if adapterdata and 'items' in adapterdata:
for adata in adapterdata['items']:
skipadapter = False
@ -305,7 +309,8 @@ class IMMClient(object):
skipadapter = True
if not skipadapter:
hwmap[aname] = bdata
self.datacache['lenovo_cached_hwmap'] = (hwmap, _monotonic_time())
self.datacache['lenovo_cached_hwmap'] = (hwmap,
util._monotonic_time())
self.weblogout()
return hwmap
@ -317,24 +322,26 @@ class IMMClient(object):
immverdata = self.parse_imm_buildinfo(rsp['data'])
bdata = {
'version': bmcver, 'build': immverdata[0], 'date': immverdata[1]}
yield ('IMM', bdata)
yield (self.bmcname, bdata)
bdata = self.fetch_grouped_properties({
'build': '/v2/ibmc/dm/fw/imm2/backup_build_id',
'version': '/v2/ibmc/dm/fw/imm2/backup_build_version',
'date': '/v2/ibmc/dm/fw/imm2/backup_build_date'})
if bdata:
yield ('IMM Backup', bdata)
yield ('{0} Backup'.format(self.bmcname), bdata)
bdata = self.fetch_grouped_properties({
'build': '/v2/ibmc/trusted_buildid',
})
if bdata:
yield ('IMM Trusted Image', bdata)
yield ('{0} Trusted Image'.format(self.bmcname), bdata)
bdata = self.fetch_grouped_properties({
'build': '/v2/bios/build_id',
'version': '/v2/bios/build_version',
'date': '/v2/bios/build_date'})
if bdata:
yield ('UEFI', bdata)
else:
yield ('UEFI', {'version': 'unknown'})
bdata = self.fetch_grouped_properties({
'build': '/v2/ibmc/dm/fw/bios/backup_build_id',
'version': '/v2/ibmc/dm/fw/bios/backup_build_version'})
@ -346,5 +353,130 @@ class IMMClient(object):
'build': '/v2/bios/pending_build_id'})
if bdata:
yield ('UEFI Pending Update', bdata)
fpga = self.ipmicmd.xraw_command(netfn=0x3a, command=0x6b, data=(0,))
fpga = '{0}.{1}.{2}'.format(*[ord(x) for x in fpga['data']])
yield ('FPGA', {'version': fpga})
for firm in self.fetch_agentless_firmware():
yield firm
class XCCClient(IMMClient):
logouturl = '/api/providers/logout'
bmcname = 'XCC'
def get_webclient(self):
cv = self.ipmicmd.certverify
wc = webclient.SecureHTTPConnection(self.imm, 443, verifycallback=cv)
try:
wc.connect()
except Exception:
return None
adata = json.dumps({'username': self.username,
'password': self.password
})
headers = {'Connection': 'keep-alive',
'Content-Type': 'application/json'}
wc.request('POST', '/api/login', adata, headers)
rsp = wc.getresponse()
if rsp.status == 200:
rspdata = json.loads(rsp.read())
wc.set_header('Content-Type', 'application/json')
wc.set_header('Authorization', 'Bearer ' + rspdata['access_token'])
if '_csrf_token' in wc.cookies:
wc.set_header('X-XSRF-TOKEN', wc.cookies['_csrf_token'])
return wc
def attach_remote_media(self, url, user, password):
proto, host, path = util.urlsplit(url)
if proto == 'smb':
proto = 'cifs'
rq = {'Option': '', 'Domain': '', 'Write': 0}
# nfs == 1, cifs == 0
if proto == 'nfs':
rq['Protocol'] = 1
rq['Url'] = '{0}:{1}'.format(host, path)
elif proto == 'cifs':
rq['Protocol'] = 0
rq['Credential'] = '{0}:{1}'.format(user, password)
rq['Url'] = '//{0}{1}'.format(host, path)
elif proto in ('http', 'https'):
rq['Protocol'] = 7
rq['Url'] = url
else:
raise Exception('TODO')
rt = self.wc.grab_json_response('/api/providers/rp_vm_remote_connect',
json.dumps(rq))
if 'return' not in rt or rt['return'] != 0:
raise Exception('Unhandled return: ' + repr(rt))
rt = self.wc.grab_json_response('/api/providers/rp_vm_remote_mountall',
'{}')
if 'return' not in rt or rt['return'] != 0:
raise Exception('Unhandled return: ' + repr(rt))
def get_firmware_inventory(self, bmcver):
# First we fetch the system firmware found in imm properties
# then check for agentless, if agentless, get adapter info using
# https, using the caller TLS verification scheme
rsp = self.ipmicmd.xraw_command(netfn=0x3a, command=0x50)
immverdata = self.parse_imm_buildinfo(rsp['data'])
bdata = {
'version': bmcver, 'build': immverdata[0], 'date': immverdata[1]}
yield (self.bmcname, bdata)
bdata = self.fetch_grouped_properties({
'build': '/v2/ibmc/dm/fw/imm3/backup_pending_build_id',
'version': '/v2/ibmc/dm/fw/imm3/backup_pending_build_version',
'date': '/v2/ibmc/dm/fw/imm3/backup_pending_build_date'})
if bdata:
yield ('{0} Backup'.format(self.bmcname), bdata)
else:
bdata = self.fetch_grouped_properties({
'build': '/v2/ibmc/dm/fw/imm3/backup_build_id',
'version': '/v2/ibmc/dm/fw/imm3/backup_build_version',
'date': '/v2/ibmc/dm/fw/imm3/backup_build_date'})
if bdata:
yield ('{0} Backup'.format(self.bmcname), bdata)
bdata = self.fetch_grouped_properties({
'build': '/v2/ibmc/trusted_buildid',
})
if bdata:
bdata = self.fetch_grouped_properties({
'build': '/v2/ibmc/trusted_buildid',
})
if bdata:
yield ('{0} Trusted Image'.format(self.bmcname), bdata)
bdata = self.fetch_grouped_properties({
'build': '/v2/bios/build_id',
'version': '/v2/bios/build_version',
'date': '/v2/bios/build_date'})
if bdata:
yield ('UEFI', bdata)
# Note that the next pending could be pending for either primary
# or backup, so can't promise where it will go
bdata = self.fetch_grouped_properties({
'build': '/v2/bios/pending_build_id'})
if bdata:
yield ('UEFI Pending Update', bdata)
bdata = self.fetch_grouped_properties({
'build': '/v2/tdm/build_id',
'version': '/v2/tdm/build_version',
'date': '/v2/tdm/build_date'})
if bdata:
yield ('LXPM', bdata)
fpga = self.ipmicmd.xraw_command(netfn=0x3a, command=0x6b, data=(0,))
fpga = '{0}.{1}.{2}'.format(*[ord(x) for x in fpga['data']])
yield ('FPGA', {'version': fpga})
for firm in self.fetch_agentless_firmware():
yield firm
def detach_remote_media(self):
rt = self.wc.grab_json_response('/api/providers/rp_vm_remote_getdisk')
if 'items' in rt:
slots = []
for mount in rt['items']:
slots.append(mount['slotId'])
for slot in slots:
rt = self.wc.grab_json_response(
'/api/providers/rp_vm_remote_unmount',
json.dumps({'Slot': slot}))
if 'return' not in rt or rt['return'] != 0:
raise Exception("Unrecognized return: " + repr(rt))

View File

@ -1,6 +1,6 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016 Lenovo
# Copyright 2016-2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -15,6 +15,7 @@
# limitations under the License.
import pyghmi.constants as pygconst
import pyghmi.exceptions as pygexc
import pyghmi.ipmi.sdr as sdr
import struct
@ -27,12 +28,16 @@ except NameError:
def fpc_read_ac_input(ipmicmd):
rsp = ipmicmd.xraw_command(netfn=0x32, command=0x90, data=(1,))
rsp = rsp['data']
if len(rsp) == 6:
rsp = b'\x00' + bytes(rsp)
return struct.unpack_from('<H', rsp[3:5])[0]
def fpc_read_dc_output(ipmicmd):
rsp = ipmicmd.xraw_command(netfn=0x32, command=0x90, data=(2,))
rsp = rsp['data']
if len(rsp) == 6:
rsp = b'\x00' + bytes(rsp)
return struct.unpack_from('<H', rsp[3:5])[0]
@ -43,16 +48,20 @@ def fpc_read_fan_power(ipmicmd):
return struct.unpack_from('<I', rsp[1:])[0] / 100.0
def fpc_read_psu_fan(ipmicmd, number):
def fpc_read_psu_fan(ipmicmd, number, sz):
rsp = ipmicmd.xraw_command(netfn=0x32, command=0xa5, data=(number,))
rsp = rsp['data']
return struct.unpack_from('<H', rsp[:2])[0]
def fpc_get_psustatus(ipmicmd, number):
def fpc_get_psustatus(ipmicmd, number, sz):
rsp = ipmicmd.xraw_command(netfn=0x32, command=0x91)
mask = 1 << (number - 1)
statdata = bytearray(rsp['data'])
if len(rsp['data']) == 6:
statdata = bytearray([0])
else:
statdata = bytearray()
statdata += bytearray(rsp['data'])
presence = statdata[3] & mask == mask
pwrgood = statdata[4] & mask == mask
throttle = (statdata[6] | statdata[2]) & mask == mask
@ -72,12 +81,23 @@ def fpc_get_psustatus(ipmicmd, number):
return (health, states)
def fpc_get_nodeperm(ipmicmd, number):
rsp = ipmicmd.xraw_command(netfn=0x32, command=0xa7, data=(number,))
def fpc_get_nodeperm(ipmicmd, number, sz):
try:
rsp = ipmicmd.xraw_command(netfn=0x32, command=0xa7, data=(number,))
except pygexc.IpmiException as ie:
if ie.ipmicode == 0xd5: # no node present
return (pygconst.Health.Ok, ['Absent'])
raise
perminfo = ord(rsp['data'][1])
health = pygconst.Health.Ok
states = []
if rsp['data'][4] in ('\x02', '\x03'):
if len(rsp['data']) == 4: # different gens handled rc differently
rsp['data'] = b'\x00' + bytes(rsp['data'])
if sz == 6: # FPC
permfail = ('\x02', '\x03')
elif sz == 2: # SMM
permfail = ('\x02',)
if rsp['data'][4] in permfail:
states.append('Insufficient Power')
health = pygconst.Health.Failed
if perminfo & 0x40:
@ -111,7 +131,7 @@ fpc_sensors = {
'type': 'Fan',
'units': 'RPM',
'provider': fpc_read_psu_fan,
'elements': 6,
'elements': 1,
},
'Total Power Capacity': {
'type': 'Power',
@ -123,36 +143,40 @@ fpc_sensors = {
'returns': 'tuple',
'units': None,
'provider': fpc_get_nodeperm,
'elements': 12,
'elements': 2,
},
'Power Supply': {
'type': 'Power Supply',
'returns': 'tuple',
'units': None,
'provider': fpc_get_psustatus,
'elements': 6,
'elements': 1,
}
}
def get_sensor_names():
def get_sensor_names(size):
global fpc_sensors
for name in fpc_sensors:
if size == 2 and name in ('Fan Power', 'Total Power Capacity'):
continue
sensor = fpc_sensors[name]
if 'elements' in sensor:
for elemidx in range(sensor['elements']):
for elemidx in range(sensor['elements'] * size):
elemidx += 1
yield '{0} {1}'.format(name, elemidx)
else:
yield name
def get_sensor_descriptions():
def get_sensor_descriptions(size):
global fpc_sensors
for name in fpc_sensors:
if size == 2 and name in ('Fan Power', 'Total Power Capacity'):
continue
sensor = fpc_sensors[name]
if 'elements' in sensor:
for elemidx in range(sensor['elements']):
for elemidx in range(sensor['elements'] * size):
elemidx += 1
yield {'name': '{0} {1}'.format(name, elemidx),
'type': sensor['type']}
@ -160,7 +184,23 @@ def get_sensor_descriptions():
yield {'name': name, 'type': sensor['type']}
def get_sensor_reading(name, ipmicmd):
def get_fpc_firmware(bmcver, ipmicmd, fpcorsmm):
mymsg = ipmicmd.xraw_command(netfn=0x32, command=0xa8)
builddata = bytearray(mymsg['data'])
name = None
if fpcorsmm == 2: # SMM
name = 'SMM'
buildid = '{0}{1}{2}{3}{4}{5}{6}'.format(
*[chr(x) for x in builddata[-7:]])
elif len(builddata) == 8:
builddata = builddata[1:] # discard the 'completion code'
name = 'FPC'
buildid = '{0}{1}'.format(builddata[-2], chr(builddata[-1]))
yield (name, {'version': bmcver, 'build': buildid})
yield ('PSOC', {'version': '{0}.{1}'.format(builddata[2], builddata[3])})
def get_sensor_reading(name, ipmicmd, sz):
value = None
sensor = None
health = pygconst.Health.Ok
@ -169,14 +209,14 @@ def get_sensor_reading(name, ipmicmd):
sensor = fpc_sensors[name]
value = sensor['provider'](ipmicmd)
else:
bname, _, idx = name.rpartition(' ')
bnam, _, idx = name.rpartition(' ')
idx = int(idx)
if bname in fpc_sensors and idx <= fpc_sensors[bname]['elements']:
sensor = fpc_sensors[bname]
if bnam in fpc_sensors and idx <= fpc_sensors[bnam]['elements'] * sz:
sensor = fpc_sensors[bnam]
if 'returns' in sensor:
health, states = sensor['provider'](ipmicmd, idx)
health, states = sensor['provider'](ipmicmd, idx, sz)
else:
value = sensor['provider'](ipmicmd, idx)
value = sensor['provider'](ipmicmd, idx, sz)
if sensor is not None:
return sdr.SensorReading({'name': name, 'imprecision': None,
'value': value, 'states': states,

View File

@ -372,6 +372,15 @@ class Session(object):
# is given in the same thread as was called
return
@classmethod
def _is_session_valid(cls, session):
sess = cls.keepalive_sessions.get(session, None)
if sess is not None and 'timeout' in sess:
if sess['timeout'] < _monotonic_time():
# session would have timed out by now, don't use it
return False
return True
def __new__(cls,
bmc,
userid,
@ -390,7 +399,8 @@ class Session(object):
self = cls.bmc_handlers[sockaddr]
if (self.bmc == bmc and self.userid == userid and
self.password == password and self.kgo == kg and
(self.logged or self.logging)):
(self.logged or self.logging) and
cls._is_session_valid(self)):
trueself = self
else:
del cls.bmc_handlers[sockaddr]

View File

@ -77,7 +77,9 @@ class SecureHTTPConnection(httplib.HTTPConnection, object):
def request(self, method, url, body=None, headers=None):
if headers is None:
headers = self.stdheaders
headers = self.stdheaders.copy()
if method == 'GET' and 'Content-Type' in headers:
del headers['Content-Type']
if self.cookies:
cookies = []
for ckey in self.cookies: