2
0
mirror of https://github.com/xcat2/confluent.git synced 2026-09-02 07:26:04 +00:00

Port the SMM handler, Delta PDU logout and XCC config to async

Taken from fix/asyncio-port-critical, limited to what pyrefly reports.

The SMM handler still used the httplib style connect/request/getresponse that
the async webclient does not have, so nothing was ever sent. It goes through
grab_response_with_status now, with allow_redirects=False to preserve httplib
behaviour, hence the new webclient parameter. That also fixes a login passing
its headers as urlencode's second positional argument.

Delta PDU's logout was a plain function both callers awaited, so the power
paths raised TypeError. XCC's set_system_configuration was the last
synchronous implementation of a method every caller awaits.
This commit is contained in:
Markus Hilger
2026-08-10 04:39:27 +02:00
parent 9148a9e1cf
commit 2ea7aed2cc
4 changed files with 62 additions and 61 deletions
@@ -206,15 +206,15 @@ class OEMHandler(generic.OEMHandler):
retcfg[opt]['sortid'] = self.fwo[opt]['sortid']
return retcfg
def set_system_configuration(self, changeset, fishclient):
async def set_system_configuration(self, changeset, fishclient):
if not self.fwc:
self.fwc = config.LenovoFirmwareConfig(self, useipmi=False)
fetchimm = False
if not self.fwo or util._monotonic_time() - self.fwovintage > 30:
try:
self.fwo = self.fwc.get_fw_options(fetchimm=fetchimm)
self.fwo = await self.fwc.get_fw_options(fetchimm=fetchimm)
except config.Unsupported:
return super(OEMHandler, self).set_system_configuration(
return await super(OEMHandler, self).set_system_configuration(
changeset, fishclient)
self.fwovintage = util._monotonic_time()
for key in list(changeset):
@@ -231,7 +231,7 @@ class OEMHandler(generic.OEMHandler):
found = True
if not found and not fetchimm:
fetchimm = True
self.fwo = self.fwc.get_fw_options(fetchimm=fetchimm)
self.fwo = await self.fwc.get_fw_options(fetchimm=fetchimm)
if key in self.fwo:
continue
else:
@@ -254,7 +254,7 @@ class OEMHandler(generic.OEMHandler):
self.merge_changeset(changeset)
if changeset:
try:
self.fwc.set_fw_options(self.fwo)
await self.fwc.set_fw_options(self.fwo)
finally:
self.fwo = None
self.fwovintage = 0
+2 -1
View File
@@ -357,7 +357,7 @@ class WebConnection:
return rsp, status
async def grab_response_with_status(self, url, data=None, referer=None,
headers=None, method=None, expect_type=None):
headers=None, method=None, expect_type=None, allow_redirects=True):
if not headers:
headers = self.stdheaders.copy()
else:
@@ -378,6 +378,7 @@ class WebConnection:
headers['Content-Type'] = 'application/json'
elif data is not None:
kwargs['data'] = data
kwargs['allow_redirects'] = allow_redirects
async with thefunc(url, headers=headers, ssl=self.ssl, **kwargs) as rsp:
if rsp.status >= 200 and rsp.status < 300:
if expect_type == 'json':
@@ -53,6 +53,20 @@ def fixuuid(baduuid):
uuid = (a[:8], a[8:12], a[12:16], baduuid[16:20], baduuid[20:])
return '-'.join(uuid).lower()
async def _post(wc, url, data, headers=None):
if headers is None:
headers = wc.stdheaders.copy()
if data:
# What WebConnection.request used to fill in, and what the SMM
# expects of its /data endpoints. grab_response_with_status adds
# nothing, so aiohttp would label a str body text/plain instead.
headers['Content-Type'] = 'application/x-www-form-urlencoded'
body, _, _ = await wc.grab_response_with_status(
url, data, headers=headers, method='POST', allow_redirects=False)
return body
class NodeHandler(bmchandler.NodeHandler):
is_enclosure = True
devname = 'SMM'
@@ -66,7 +80,7 @@ class NodeHandler(bmchandler.NodeHandler):
uuid = fixuuid(uuid[0])
self.info['uuid'] = uuid
def _webconfigrules(self, wc):
async def _webconfigrules(self, wc):
rules = []
for rule in self.ruleset.split(','):
if '=' not in rule:
@@ -84,19 +98,16 @@ class NodeHandler(bmchandler.NodeHandler):
rules.append('passwordReuseCheckNum:' + value)
if rules:
apirequest = 'set={0}'.format(','.join(rules))
wc.request('POST', '/data', apirequest)
wc.getresponse().read()
await _post(wc, '/data', apirequest)
async def _webconfignet(self, wc, nodename):
cfg = self.configmanager
if 'service:lenovo-smm2' in self.info.get('services', []):
# need to enable ipmi for now..
wc.request('POST', '/data', 'set=DoCmd(0x06,0x40,0x01,0x82,0x84)')
rsp = wc.getresponse()
rsp.read()
wc.request('POST', '/data', 'set=DoCmd(0x06,0x40,0x01,0x42,0x44)')
rsp = wc.getresponse()
rsp.read()
await _post(wc, '/data',
'set=DoCmd(0x06,0x40,0x01,0x82,0x84)')
await _post(wc, '/data',
'set=DoCmd(0x06,0x40,0x01,0x42,0x44)')
cd = cfg.get_node_attributes(
nodename, ['hardwaremanagement.manager'])
smmip = cd.get(nodename, {}).get('hardwaremanagement.manager', {}).get('value', None)
@@ -107,9 +118,8 @@ class NodeHandler(bmchandler.NodeHandler):
smmip = smmip[-1][0]
if smmip and ':' in smmip:
raise exc.NotImplementedException('IPv6 not supported')
wc.request('POST', '/data', 'get=hostname')
rsp = wc.getresponse()
rspdata = fromstring(util.stringify(rsp.read()))
rspdata = fromstring(util.stringify(
await _post(wc, '/data', 'get=hostname')))
currip = rspdata.find('netConfig').find('ifConfigEntries').find(
'ifConfig').find('v4IPAddr').text
if currip == smmip:
@@ -120,9 +130,7 @@ class NodeHandler(bmchandler.NodeHandler):
gateway = netconfig.get('ipv4_gateway', None)
if gateway:
setdata += ',v4Gateway:{0}'.format(gateway)
wc.request('POST', '/data', setdata)
rsp = wc.getresponse()
rspdata = util.stringify(rsp.read())
rspdata = util.stringify(await _post(wc, '/data', setdata))
if '<statusCode>0' not in rspdata:
raise Exception("Error configuring SMM Network")
return
@@ -132,48 +140,45 @@ class NodeHandler(bmchandler.NodeHandler):
await cfg.set_node_attributes(
{nodename: {'hardwaremanagement.manager': self.ipaddr}})
def _webconfigcreds(self, username, password):
ip, port = self.get_web_port_and_ip()
async def _webconfigcreds(self, username, password):
ip, port = await self.get_web_port_and_ip()
wc = webclient.WebConnection(ip, port, verifycallback=self.validate_cert)
wc.connect()
authdata = { # start by trying factory defaults
'user': 'USERID',
'password': 'PASSW0RD',
}
headers = {'Connection': 'keep-alive', 'Content-Type': 'application/x-www-form-urlencoded'}
wc.request('POST', '/data/login', urlencode(authdata), headers)
rsp = wc.getresponse()
rspdata = util.stringify(rsp.read())
rspdata = util.stringify(
await _post(wc, '/data/login', urlencode(authdata), headers))
if 'authResult>0' not in rspdata:
# default credentials are refused, try with the actual
authdata['user'] = username
authdata['password'] = password
wc.request('POST', '/data/login', urlencode(authdata), headers)
rsp = wc.getresponse()
rspdata = util.stringify(rsp.read())
rspdata = util.stringify(
await _post(wc, '/data/login', urlencode(authdata), headers))
if 'renew_account' in rspdata:
tmppassword = 'Tmp42' + password[5:]
tokens = fromstring(rspdata)
st2 = tokens.findall('st2')[0].text
wc.set_header('ST2', st2)
wc.request('POST', '/data/changepwd', 'oripwd={0}&newpwd={1}'.format(password, tmppassword))
rsp = wc.getresponse()
rspdata = rsp.read().decode('utf8')
rspdata = await _post(
wc, '/data/changepwd',
'oripwd={0}&newpwd={1}'.format(password, tmppassword))
rspdata = rspdata.decode('utf8')
bdata = 'user={0}&password={1}'.format(username, tmppassword)
wc.request('POST', '/data/login', bdata, headers)
rsp = wc.getresponse()
rspdata = rsp.read().decode('utf8')
rspdata = await _post(wc, '/data/login', bdata, headers)
rspdata = rspdata.decode('utf8')
tokens = fromstring(rspdata)
st2 = tokens.findall('st2')[0].text
wc.set_header('ST2', st2)
rules = 'set=passwordChangeInterval:0,passwordReuseCheckNum:0'
wc.request('POST', '/data', rules)
wc.getresponse().read()
wc.request('POST', '/data/changepwd', 'oripwd={0}&newpwd={1}'.format(tmppassword, password))
wc.getresponse().read()
wc.request('POST', '/data/login', urlencode(authdata), headers)
rsp = wc.getresponse()
rspdata = util.stringify(rsp.read())
await _post(wc, '/data', rules)
await _post(
wc, '/data/changepwd',
'oripwd={0}&newpwd={1}'.format(tmppassword, password))
rspdata = util.stringify(
await _post(wc, '/data/login', urlencode(authdata),
headers))
if 'authResult>0' not in rspdata:
raise Exception('Unknown username/password on SMM')
tokens = fromstring(rspdata)
@@ -185,29 +190,24 @@ class NodeHandler(bmchandler.NodeHandler):
tokens = fromstring(rspdata)
st2 = tokens.findall('st2')[0].text
wc.set_header('ST2', st2)
wc.request('POST', '/data/changepwd', urlencode(passwdchange))
rsp = wc.getresponse()
rspdata = rsp.read()
rspdata = await _post(
wc, '/data/changepwd', urlencode(passwdchange))
authdata['password'] = password
wc.request('POST', '/data/login', urlencode(authdata), headers)
rsp = wc.getresponse()
rspdata = util.stringify(rsp.read())
rspdata = util.stringify(
await _post(wc, '/data/login', urlencode(authdata), headers))
if 'authResult>0' in rspdata:
tokens = fromstring(rspdata)
st2 = tokens.findall('st2')[0].text
wc.set_header('ST2', st2)
if username == 'USERID':
return wc
wc.request('POST', '/data', 'set=user(2,1,{0},511,,4,15,0)'.format(username))
rsp = wc.getresponse()
rspdata = rsp.read()
wc.request('POST', '/data/logout')
rsp = wc.getresponse()
rspdata = rsp.read()
rspdata = await _post(
wc, '/data',
'set=user(2,1,{0},511,,4,15,0)'.format(username))
rspdata = await _post(wc, '/data/logout', None)
authdata['user'] = username
wc.request('POST', '/data/login', urlencode(authdata, headers))
rsp = wc.getresponse()
rspdata = rsp.read()
rspdata = await _post(
wc, '/data/login', urlencode(authdata), headers)
tokens = fromstring(rspdata)
st2 = tokens.findall('st2')[0].text
wc.set_header('ST2', st2)
@@ -249,8 +249,8 @@ class NodeHandler(bmchandler.NodeHandler):
raise Exception('Using the default password is no longer supported')
else:
# Switch to full web based configuration, to mitigate risks with the SMM
wc = self._webconfigcreds(username, passwd)
self._webconfigrules(wc)
wc = await self._webconfigcreds(username, passwd)
await self._webconfigrules(wc)
await self._webconfignet(wc, nodename)
@@ -158,7 +158,7 @@ class PDUClient(object):
raise exc.TargetEndpointBadCredentials()
def logout(self):
async def logout(self):
self.wc.grab_response('/logout_wait.htm')
async def get_outlet(self, outlet):