diff --git a/confluent_server/aiohmi/ipmi/oem/lenovo/handler.py b/confluent_server/aiohmi/ipmi/oem/lenovo/handler.py index d8833ed4..2999ebdf 100755 --- a/confluent_server/aiohmi/ipmi/oem/lenovo/handler.py +++ b/confluent_server/aiohmi/ipmi/oem/lenovo/handler.py @@ -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. diff --git a/confluent_server/aiohmi/ipmi/oem/lenovo/nextscale.py b/confluent_server/aiohmi/ipmi/oem/lenovo/nextscale.py index 9fa64fc7..2458ecbd 100644 --- a/confluent_server/aiohmi/ipmi/oem/lenovo/nextscale.py +++ b/confluent_server/aiohmi/ipmi/oem/lenovo/nextscale.py @@ -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 'ok' 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 'ok' 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