2
0
mirror of https://github.com/xcat2/confluent.git synced 2026-08-03 07:57:02 +00:00

Merge pull request #257 from Obihoernchen/lenovo-async

Fix the NextScale SMM web path on the asyncio port
This commit is contained in:
Jarrod Johnson
2026-07-28 08:48:50 -04:00
committed by GitHub
2 changed files with 185 additions and 170 deletions
@@ -360,7 +360,7 @@ class OEMHandler(generic.OEMHandler):
srvs.append(ntpres['data'][129:257].rstrip('\x00'))
return srvs
if await self.is_fpc():
return self.smmhandler.get_ntp_servers()
return await self.smmhandler.get_ntp_servers()
if self.has_tsma:
return await self.tsmahandler.get_ntp_servers()
return ()
@@ -375,7 +375,7 @@ class OEMHandler(generic.OEMHandler):
netfn=0x32, command=0xa8, data=(3, 0), timeout=15)
return True
if await self.is_fpc():
self.smmhandler.set_ntp_enabled(enabled)
await self.smmhandler.set_ntp_enabled(enabled)
return True
if self.has_tsma:
await self.tsmahandler.set_ntp_enabled(enabled)
@@ -393,7 +393,7 @@ class OEMHandler(generic.OEMHandler):
if not 0 <= index <= 2:
raise pygexc.InvalidParameterValue(
'SMM supports indexes 0 through 2')
self.smmhandler.set_ntp_server(server, index)
await self.smmhandler.set_ntp_server(server, index)
return True
elif self.has_tsma:
if not (0 <= index <= 1):
@@ -940,7 +940,7 @@ class OEMHandler(generic.OEMHandler):
name += rsp['data'][:]
return name.rstrip('\x00')
elif await self.is_fpc():
return self.smmhandler.get_domain()
return await self.smmhandler.get_domain()
async def set_oem_domain_name(self, name):
if await self.has_tsm():
@@ -959,20 +959,20 @@ class OEMHandler(generic.OEMHandler):
await self._restart_dns()
return
elif await self.is_fpc():
self.smmhandler.set_domain(name)
await self.smmhandler.set_domain(name)
async def set_hostname(self, hostname):
if await self.has_xcc():
return await self.immhandler.set_hostname(hostname)
elif await self.is_fpc():
return self.smmhandler.set_hostname(hostname)
return await self.smmhandler.set_hostname(hostname)
return await super(OEMHandler, self).set_hostname(hostname)
async def get_hostname(self):
if await self.has_xcc():
return await self.immhandler.get_hostname()
elif await self.is_fpc():
return self.smmhandler.get_hostname()
return await self.smmhandler.get_hostname()
return await super(OEMHandler, self).get_hostname()
""" Gets a remote console launcher for a Lenovo ThinkServer.
@@ -384,6 +384,8 @@ class SMMClient(object):
self.username = ipmicmd.ipmi_session.userid
self.password = ipmicmd.ipmi_session.password
self._wc = None
self.weblogging = False
self.updating = False
async def clear_bmc_configuration(self):
await self.ipmicmd.raw_command(0x32, 0xad)
@@ -409,15 +411,14 @@ class SMMClient(object):
async def get_bmc_configuration(self, variant):
settings = {}
wc = self.wc
wc.request(
'POST', '/data',
rspbody, status = await self.webrequest(
'/data',
('get=passwordMinLength,passwordForceChange,passwordDurationDays,'
'passwordExpireWarningDays,passwordChangeInterval,'
'passwordReuseCheckNum,passwordFailAllowdNum,'
'passwordLockoutTimePeriod,timeZone'))
rsp = wc.getresponse()
rspbody = rsp.read()
if status != 200:
raise Exception(rspbody.decode('utf8', 'replace'))
accountinfo = fromstring(rspbody)
for rule in self.rulemap:
ruleinfo = accountinfo.find(self.rulemap[rule])
@@ -693,9 +694,9 @@ class SMMClient(object):
changeset[key]['value']))
if rules:
rules = 'set={0}'.format(','.join(rules))
wc = self.wc
wc.request('POST', '/data', rules)
wc.getresponse().read()
rsp, status = await self.webrequest('/data', rules)
if status != 200:
raise Exception(rsp.decode('utf8', 'replace'))
if powercfg != [None, None]:
if variant != 6:
if None in powercfg:
@@ -734,12 +735,11 @@ class SMMClient(object):
username = bytes(rsp['data']).rstrip(b'\x00')
if not isinstance(username, str):
username = username.decode('utf8')
wc = self.wc
wc.request(
'POST', '/data', 'set=user({0},1,{1},511,,4,15,0)'.format(
rsp, status = await self.webrequest(
'/data', 'set=user({0},1,{1},511,,4,15,0)'.format(
uid, username))
rsp = wc.getresponse()
rsp.read()
if status != 200:
raise Exception(rsp.decode('utf8', 'replace'))
async def reseat_bay(self, bay):
bay = int(bay)
@@ -791,24 +791,27 @@ class SMMClient(object):
rsp = await self.ipmicmd.raw_command(netfn=0x34, command=0x12, data=[1])
if progress:
progress({'phase': 'initializing', 'progress': initpct})
wc = self.wc
if wc is None:
raise Exception("Failed to connect to web api")
wc = await self.wc()
if variant and variant >> 5:
url = '/preview/smm2-ffdc.tgz?ST1={0}'.format(wc.st1)
else:
url = '/preview/smm-ffdc.tgz?ST1={0}'.format(wc.st1)
if autosuffix and not savefile.endswith('.tgz'):
savefile += '-smm-ffdc.tgz'
fd = webclient.make_downloader(wc, url, savefile)
while not fd.completed():
try:
await fd.join(1)
except asyncio.TimeoutError:
pass
if progress and await fd.get_progress():
progress({'phase': 'download',
'progress': 100 * await fd.get_progress()})
# the download runs on this session, keep wc() from logging it out
self.updating = True
try:
fd = webclient.make_downloader(wc, url, savefile)
while not fd.completed():
try:
await fd.join(1)
except asyncio.TimeoutError:
pass
if progress and await fd.get_progress():
progress({'phase': 'download',
'progress': 100 * await fd.get_progress()})
finally:
self.updating = False
if progress:
progress({'phase': 'complete'})
return savefile
@@ -827,23 +830,19 @@ class SMMClient(object):
fru['Model'] = mnum.strip(b' \x00\xff').replace(b'\xff', b'')
return fru
def get_webclient(self):
async def get_webclient(self):
cv = self.ipmicmd.certverify
wc = webclient.SecureHTTPConnection(self.smm, 443, verifycallback=cv)
wc = webclient.WebConnection(self.smm, 443, verifycallback=cv)
wc.vintage = util._monotonic_time()
wc.connect()
wc.set_header('Content-Type', 'application/x-www-form-urlencoded')
loginform = urlencode(
{
'user': self.username,
'password': self.password
}
)
wc.request('POST', '/data/login', loginform)
rsp = wc.getresponse()
if rsp.status != 200:
raise Exception(rsp.read())
authdata = rsp.read()
authdata, status, _ = await wc.grab_response_with_status('/data/login', loginform)
if status != 200:
raise Exception(authdata.decode('utf8', 'replace'))
authdata = fromstring(authdata)
for data in authdata.findall('authResult'):
if int(data.text) != 0:
@@ -860,11 +859,9 @@ class SMMClient(object):
wc.st2 = data.text
if not wc.st2:
# This firmware puts tokens in the html file, parse that
wc.request('GET', '/index.html')
rsp = wc.getresponse()
if rsp.status != 200:
raise Exception(rsp.read())
indexhtml = rsp.read()
indexhtml, status, _ = await wc.grab_response_with_status('/index.html', method='GET')
if status != 200:
raise Exception(indexhtml.decode('utf8', 'replace'))
if not isinstance(indexhtml, str):
indexhtml = indexhtml.decode('utf8')
for line in indexhtml.split('\n'):
@@ -875,11 +872,9 @@ class SMMClient(object):
wc.st2 = line.split()[-1].replace(
'"', '').replace(',', '')
if not wc.st2:
wc.request('GET', '/scripts/index.ajs')
rsp = wc.getresponse()
body = rsp.read()
if rsp.status != 200:
raise Exception(body)
body, status, _ = await wc.grab_response_with_status('/scripts/index.ajs', method='GET')
if status != 200:
raise Exception(body.decode('utf8', 'replace'))
if not isinstance(body, str):
body = body.decode('utf8')
for line in body.split('\n'):
@@ -892,92 +887,75 @@ class SMMClient(object):
if not wc.st2:
raise Exception('Unable to locate ST2 token')
wc.set_header('ST2', wc.st2)
wc.vintage = util._monotonic_time()
return wc
def set_hostname(self, hostname):
wc = self.wc
wc.request('POST', '/data', 'set=hostname:' + hostname)
rsp = wc.getresponse()
if rsp.status != 200:
raise Exception(rsp.read())
rsp.read()
self.logout()
async def set_hostname(self, hostname):
rsp, status = await self.webrequest(
'/data', 'set=hostname:' + hostname)
if status != 200:
raise Exception(rsp.decode('utf8', 'replace'))
def get_hostname(self):
currinfo = self.get_netinfo()
self.logout()
async def get_hostname(self):
currinfo = await self.get_netinfo()
for data in currinfo.find('netConfig').findall('hostname'):
return data.text
def get_netinfo(self):
wc = self.wc
wc.request('POST', '/data', 'get=hostname')
rsp = wc.getresponse()
data = rsp.read()
if rsp.status == 400:
wc.request('POST', '/data?get=hostname', '')
rsp = wc.getresponse()
data = rsp.read()
if rsp.status != 200:
raise Exception(data)
async def get_netinfo(self):
data, status = await self.webrequest('/data', 'get=hostname')
if status == 400:
data, status = await self.webrequest('/data?get=hostname', '')
if status != 200:
raise Exception(data.decode('utf8', 'replace'))
currinfo = fromstring(data)
return currinfo
def set_domain(self, domain):
wc = self.wc
wc.request('POST', '/data', 'set=dnsDomain:' + domain)
rsp = wc.getresponse()
if rsp.status != 200:
raise Exception(rsp.read())
rsp.read()
self.logout()
async def set_domain(self, domain):
rsp, status = await self.webrequest(
'/data', 'set=dnsDomain:' + domain)
if status != 200:
raise Exception(rsp.decode('utf8', 'replace'))
def get_domain(self):
currinfo = self.get_netinfo()
self.logout()
async def get_domain(self):
currinfo = await self.get_netinfo()
for data in currinfo.find('netConfig').findall('dnsDomain'):
return data.text
def get_ntp_enabled(self, variant):
wc = self.wc
wc.request('POST', '/data', 'get=ntpOpMode')
rsp = wc.getresponse()
info = fromstring(rsp.read())
self.logout()
async def get_ntp_enabled(self, variant):
rsp, status = await self.webrequest('/data', 'get=ntpOpMode')
if status != 200:
raise Exception(rsp.decode('utf8', 'replace'))
info = fromstring(rsp)
for data in info.findall('ntpOpMode'):
return data.text == '1'
def set_ntp_enabled(self, enabled):
wc = self.wc
wc.request('POST', '/data', 'set=ntpOpMode:{0}'.format(
1 if enabled else 0))
rsp = wc.getresponse()
result = rsp.read()
async def set_ntp_enabled(self, enabled):
result, status = await self.webrequest(
'/data', 'set=ntpOpMode:{0}'.format(1 if enabled else 0))
if status != 200:
raise Exception(result.decode('utf8', 'replace'))
if not isinstance(result, str):
result = result.decode('utf8')
self.logout()
if '<status>ok</status>' not in result:
raise Exception("Unrecognized result: " + result)
def set_ntp_server(self, server, index):
wc = self.wc
wc.request('POST', '/data', 'set=ntpServer{0}:{1}'.format(
index + 1, server))
rsp = wc.getresponse()
result = rsp.read()
async def set_ntp_server(self, server, index):
result, status = await self.webrequest(
'/data', 'set=ntpServer{0}:{1}'.format(index + 1, server))
if status != 200:
raise Exception(result.decode('utf8', 'replace'))
if not isinstance(result, str):
result = result.decode('utf8')
if '<status>ok</status>' not in result:
raise Exception("Unrecognized result: " + result)
self.logout()
return True
def get_ntp_servers(self):
wc = self.wc
wc.request(
'POST', '/data', 'get=ntpServer1,ntpServer2,ntpServer3')
rsp = wc.getresponse()
result = fromstring(rsp.read())
async def get_ntp_servers(self):
rsp, status = await self.webrequest(
'/data', 'get=ntpServer1,ntpServer2,ntpServer3')
if status != 200:
raise Exception(rsp.decode('utf8', 'replace'))
result = fromstring(rsp)
srvs = []
for data in result.findall('ntpServer1'):
srvs.append(data.text)
@@ -985,7 +963,6 @@ class SMMClient(object):
srvs.append(data.text)
for data in result.findall('ntpServer3'):
srvs.append(data.text)
self.logout()
return srvs
async def update_firmware(self, filename, data=None, progress=None, bank=None):
@@ -1007,58 +984,72 @@ class SMMClient(object):
data = z.open(filename)
break
progress({'phase': 'upload', 'progress': 0.0})
wc = self.wc
wc.request('POST', '/data', 'set=fwType:10') # SMM firmware
rsp = wc.getresponse()
rsp.read()
url = '/fwupload/fwupload.esp?ST1={0}'.format(wc.st1)
fu = await webclient.make_uploader(
wc, url, filename, data, formname='fileUpload',
otherfields={'preConfig': 'on'})
while not fu.completed():
try:
await fu.join(3)
except asyncio.TimeoutError:
pass
if progress:
progress({'phase': 'upload',
'progress': 100 * await fu.get_progress()})
progress({'phase': 'validating', 'progress': 0.0})
url = '/data'
wc.request('POST', url, 'get=fwVersion,spfwInfo')
rsp = wc.getresponse()
rsp.read()
if rsp.status != 200:
raise Exception('Error validating firmware')
progress({'phase': 'apply', 'progress': 0.0})
wc.request('POST', '/data', 'set=securityrollback:1')
wc.getresponse().read()
wc.request('POST', '/data', 'set=fwUpdate:1')
rsp = wc.getresponse()
rsp.read()
complete = False
tries = 0
while not complete:
await ipmisession.Session.pause(3)
wc.request('POST', '/data', 'get=fwProgress,fwUpdate')
try:
rsp = wc.getresponse()
progdata = rsp.read()
except Exception:
if tries > 2:
break
tries += 1
continue
if rsp.status != 200:
raise Exception('Error applying firmware')
progdata = fromstring(progdata)
if progdata.findall('fwUpdate')[0].text == 'invalid signature':
raise Exception('Firmware signature invalid')
percent = float(progdata.findall('fwProgress')[0].text)
wc = await self.wc()
# the update runs on this session, keep wc() from logging it out
self.updating = True
try:
rsp, status, _ = await wc.grab_response_with_status(
'/data', 'set=fwType:10') # SMM firmware
if status != 200:
raise Exception(rsp.decode('utf8', 'replace'))
url = '/fwupload/fwupload.esp?ST1={0}'.format(wc.st1)
fu = await webclient.make_uploader(
wc, url, filename, data, formname='fileUpload',
otherfields={'preConfig': 'on'})
while not fu.completed():
try:
await fu.join(3)
except asyncio.TimeoutError:
pass
if progress:
progress({'phase': 'upload',
'progress': 100 * await fu.get_progress()})
progress({'phase': 'validating', 'progress': 0.0})
url = '/data'
rsp, status, _ = await wc.grab_response_with_status(
url, 'get=fwVersion,spfwInfo')
if status != 200:
raise Exception('Error validating firmware')
progress({'phase': 'apply', 'progress': 0.0})
# only understood by newer SMM2 firmware, ignore rejection by older
await wc.grab_response_with_status(
'/data', 'set=securityrollback:1')
rsp, status, _ = await wc.grab_response_with_status(
'/data', 'set=fwUpdate:1')
if status != 200:
raise Exception(rsp.decode('utf8', 'replace'))
complete = False
tries = 0
while not complete:
await ipmisession.Session.pause(3)
try:
progdata, status, _ = await wc.grab_response_with_status(
'/data', 'get=fwProgress,fwUpdate')
except Exception:
if tries > 2:
raise
tries += 1
continue
if status != 200:
# an SMM restarting its web service part way through the
# apply answers for a while before it stops answering at
# all, so spend the same budget on this as on a poll that
# went unanswered
if tries > 2:
raise Exception('Error applying firmware')
tries += 1
continue
tries = 0
progdata = fromstring(progdata)
if progdata.findall('fwUpdate')[0].text == 'invalid signature':
raise Exception('Firmware signature invalid')
percent = float(progdata.findall('fwProgress')[0].text)
progress({'phase': 'apply',
'progress': percent})
complete = percent >= 100.0
progress({'phase': 'apply',
'progress': percent})
complete = percent >= 100.0
finally:
self.updating = False
return 'complete'
async def get_inventory_descriptions(self, ipmicmd, variant):
@@ -1105,15 +1096,39 @@ class SMMClient(object):
b' \x00\xff').decode('utf8'))
return psui
def logout(self):
wc = self.wc
wc.request('POST', '/data/logout', None)
rsp = wc.getresponse()
rsp.read()
async def logout(self):
wc = self._wc
self._wc = None
if wc is None:
return
# best effort, a stale session must not fail the caller's operation
try:
await wc.grab_response_with_status('/data/logout', None, method='POST')
except Exception:
pass
async def wc(self):
if (not self._wc or self._wc.broken
or self._wc.vintage < util._monotonic_time() + 30):
self._wc = await self.get_webclient()
while self.weblogging:
await ipmisession.Session.pause(0.25)
self.weblogging = True
try:
if (not self._wc or (self._wc.vintage
and self._wc.vintage < util._monotonic_time() - 30)):
if not self.updating and self._wc:
# in case the existing session is still valid, dispose
# of it
await self.logout()
self._wc = await self.get_webclient()
finally:
self.weblogging = False
return self._wc
async def webrequest(self, url, data):
wc = await self.wc()
rsp, status, _ = await wc.grab_response_with_status(url, data)
if status == 401:
# the SMM dropped the session, log back in and try once more
self._wc = None
wc = await self.wc()
rsp, status, _ = await wc.grab_response_with_status(url, data)
return rsp, status