# 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. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import fnmatch import struct import weakref from xml.etree.ElementTree import fromstring as rfromstring import zipfile import six import pyghmi.constants as pygconst import pyghmi.exceptions as pygexc import pyghmi.ipmi.private.session as ipmisession import pyghmi.ipmi.private.util as util from pyghmi.ipmi import sdr import pyghmi.util.webclient as webclient try: from urllib import urlencode except ImportError: from urllib.parse import urlencode try: range = xrange except NameError: pass psutypes = { 0x63: 'CFF v3 550W PT', 0x64: 'CFF v3 750W PT', 0x65: 'CFF v3 750W TT', 0x66: 'CFF v3 1100W PT', 0x67: 'CFF v3 1600W PT', 0x68: 'CFF v3 2000W PT', 0x6B: 'CFF v4 500W PT', 0x6C: 'CFF v4 750W PT', 0x6D: 'CFF v4 750W TT', 0x6E: 'CFF v4 1100W PT', 0x6F: 'CFF v4 1100W -48Vdc', 0x70: 'CFF v4 1100W 200-277Vac/240-380Vdc', 0x71: 'CFF v4 1800W PT', 0x72: 'CFF v4 2400W PT', } def fromstring(inputdata): if b'!entity' in inputdata.lower(): raise Exception('!ENTITY not supported in this interface') return rfromstring(inputdata) def stringtoboolean(originput, name): input = originput.lower() try: num = int(input) except ValueError: num = None if 'enabled'.startswith(input) or 'yes'.startswith(input) or num == 1: return True elif 'disabled'.startswith(input) or 'no'.startswith(input) or num == 0: return False raise pygexc.InvalidParameterValue('{0} is an invalid setting for ' '{1}'.format(originput, name)) 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(' 5: return struct.unpack_from('= 10: tmpdata = rsp['data'] rsp['data'] = list(struct.unpack('> 5: name = 'SMM2' buildid = '{0}{1}{2}{3}{4}{5}{6}'.format( *[chr(x) for x in builddata[6:13]]) else: name = 'SMM' buildid = '{0}{1}{2}{3}{4}{5}{6}'.format( *[chr(x) for x in builddata[5:12]]) elif len(builddata) == 8: builddata = builddata[1:] # discard the 'completion code' name = 'FPC' buildid = '{0:02X}{1}'.format(builddata[-2], chr(builddata[-1])) bmcmajor, bmcminor = [int(x) for x in bmcver.split('.')] bmcver = '{0}.{1:02d}'.format(bmcmajor, bmcminor) yield (name, {'version': bmcver, 'build': buildid}) if fpcorsmm == 6: yield ('PSOC', {'version': '{0}.{1}'.format(builddata[2], builddata[3])}) else: yield ('PSOC', {'version': '{0}.{1}'.format(builddata[3], builddata[4])}) def get_sensor_reading(name, ipmicmd, sz): value = None sensor = None health = pygconst.Health.Ok states = [] if name in fpc_sensors and 'elements' not in fpc_sensors[name]: sensor = fpc_sensors[name] value = sensor['provider'](ipmicmd) else: bnam, _, idx = name.rpartition(' ') idx = int(idx) if bnam in fpc_sensors: max = -1 if 'abselements' in fpc_sensors[bnam]: max = fpc_sensors[bnam]['abselements'] elif 'elementsfun' in fpc_sensors[bnam]: max = 99 elif 'elements' in fpc_sensors[bnam]: max = fpc_sensors[bnam]['elements'] * sz if idx <= max: sensor = fpc_sensors[bnam] if 'returns' in sensor: health, states = sensor['provider'](ipmicmd, idx, sz) else: value = sensor['provider'](ipmicmd, idx, sz) if sensor is not None: return sdr.SensorReading({'name': name, 'imprecision': None, 'value': value, 'states': states, 'state_ids': [], 'health': health, 'type': sensor['type']}, sensor['units']) raise Exception('Sensor not found: ' + name) class SMMClient(object): def __init__(self, ipmicmd, variant): self.smm_variant = variant self.ipmicmd = weakref.proxy(ipmicmd) self.smm = ipmicmd.bmc self.username = ipmicmd.ipmi_session.userid self.password = ipmicmd.ipmi_session.password self._wc = None def clear_bmc_configuration(self): self.ipmicmd.xraw_command(0x32, 0xad) rulemap = { 'password_reuse_count': 'passwordReuseCheckNum', 'password_change_interval': 'passwordChangeInterval', 'password_expiration': 'passwordDurationDays', 'password_login_failures': 'passwordFailAllowdNum', 'password_min_length': 'passwordMinLength', 'password_lockout_period': 'passwordLockoutTimePeriod', 'timezone': 'timeZone', } fanmodes = { 1: 'Capped_20%', 2: 'Capped_25%', 3: 'Capped_30%', 4: 'Capped_45%', 0: 'Normal', 5: 'Boosted', } def get_bmc_configuration(self, variant): settings = {} wc = self.wc wc.request( 'POST', '/data', ('get=passwordMinLength,passwordForceChange,passwordDurationDays,' 'passwordExpireWarningDays,passwordChangeInterval,' 'passwordReuseCheckNum,passwordFailAllowdNum,' 'passwordLockoutTimePeriod,timeZone')) rsp = wc.getresponse() rspbody = rsp.read() accountinfo = fromstring(rspbody) for rule in self.rulemap: ruleinfo = accountinfo.find(self.rulemap[rule]) if ruleinfo is not None: try: val = int(ruleinfo.text) except ValueError: val = ruleinfo.text settings[rule] = {'value': val} dwc = self.ipmicmd.xraw_command(0x32, 0x94) dwc = bytearray(dwc['data']) if len(dwc) not in (3, 4) or dwc[0] == 1: rsp = self.ipmicmd.xraw_command(0x34, 3) fanmode = self.fanmodes[bytearray(rsp['data'])[0]] settings['fanspeed'] = { 'value': fanmode, 'default': 'Normal', 'help': ('Adjust the fan speed of the D2 Chassis. Capped ' 'settings will reduce fan speed for better acoustic ' 'experience at the expense of performance. Normal ' 'is using the Lenovo engineered cooling adjustments ' 'across the full range. Boosted adds fanspeed to ' 'the Normal response to provide more aggressive ' 'cooling.'), 'possible': [self.fanmodes[x] for x in self.fanmodes]} powercfg = self.ipmicmd.xraw_command(0x32, 0xa2) powercfg = bytearray(powercfg['data']) if len(powercfg) == 5: if variant and variant >> 5: powercfg = powercfg[-2:] else: powercfg = powercfg[1:] val = powercfg[0] if val == 2: val = 'N+N' elif val == 1: val = 'N+1' elif val == 0: val = 'Disable' settings['power_redundancy'] = { 'default': 'N+1', 'value': val, 'possible': ['N+N', 'N+1', 'Disable'], 'help': ('Configures allowed power budget according to expected ' 'redundancy. If N+1, power caps will be set to keep ' 'servers from using more power than the installed power ' 'supplies could supply if one fails. If disabled, ' 'power budget is set to allow nodes to exceed the ' 'capacity of a single power supply') } ovs = powercfg[1] if ovs == 1: ovs = 'Enable' elif ovs == 0: ovs = 'Disable' settings['power_oversubscription'] = { 'default': 'Enable', 'value': ovs, 'possible': ['Enable', 'Disable'], 'help': ('In redundant power configuration, permit the power ' 'budget to exceed the capacity of a single power supply ' 'so long as both power supplies are functioning. This ' 'excess is limited to an amount that the remaining power ' 'supply can sustain for a brief period of time in the ' 'event of losing the other. This excess will be removed ' 'at the moment a power supply fails so that power ' 'delivery is at the sustained capacity of the remaining ' 'supplies.') } try: numbays = 12 try: chassiscapinfo = self.ipmicmd.xraw_command( 0x32, 0x9d, data=[13]) except pygexc.IpmiException as e: if e.ipmicode == 201: # this must be a 2U numbays = 4 chassiscapinfo = self.ipmicmd.xraw_command( 0x32, 0x9d, data=[5]) else: raise retoffset = 0 if len(chassiscapinfo['data']) > 10: retoffset = 1 chassiscapstate = self.ipmicmd.xraw_command( 0x32, 0xa0, data=[numbays + 1]) capstate = bool(chassiscapstate['data'][retoffset]) capmin, capmax, protcap, usercap, thermcap = struct.unpack( '> 5 and len(currcfg) == 5: currcfg = currcfg[-2:] if powercfg[0] is None: powercfg[0] = currcfg[0] if powercfg[1] is None: powercfg[1] = currcfg[1] self.ipmicmd.xraw_command(0x32, 0xa3, data=powercfg) elif variant == 6: if powercfg[0] is not None: self.ipmicmd.xraw_command(0x32, 0xa3, data=powercfg[:1]) if powercfg[1] is not None: self.ipmicmd.xraw_command(0x32, 0x9c, data=powercfg[1:]) if sendhost is not None: sendhost = 1 if sendhost else 0 self.ipmicmd.xraw_command(0xc, 1, data=[1, 0xc5, sendhost]) if sendvci is not None: sendvci = 1 if sendvci else 0 self.ipmicmd.xraw_command(0xc, 1, data=[1, 0xc6, sendvci]) if newserial: newserial = newserial.ljust(10).encode('utf8') cmdata = b'\x05\x01' + newserial self.ipmicmd.xraw_command(0x32, 0xaf, data=cmdata) if newmodel: newmodel = newmodel.ljust(10).encode('utf8') cmdata = b'\x05\x00' + newmodel self.ipmicmd.xraw_command(0x32, 0xaf, data=cmdata) def set_user_priv(self, uid, priv): if priv.lower() == 'administrator': rsp = self.ipmicmd.xraw_command(netfn=6, command=0x46, data=(uid,)) 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( uid, username)) rsp = wc.getresponse() rsp.read() def reseat_bay(self, bay): bay = int(bay) if bay == -1: self.ipmicmd.xraw_command(0x32, 0xf5) return if bay % 2 == 0: # even node may be unable to reseat based on shared io try: rsp = self.ipmicmd.xraw_command(0x32, 0xc5, data=[1]) except Exception: rsp = {'data': [1]} rsp['data'] = bytearray(rsp['data']) if rsp['data'][0] == 2: # shared io rsp = self.ipmicmd.xraw_command(0x32, 0xa7, data=[bay - 1]) rsp['data'] = bytearray(rsp['data']) if rsp['data'][1] == 0x80: raise Exception('Unable to reseat bay {0} due to bay {1} ' 'being on with shared IO'.format( bay, bay - 1)) self.ipmicmd.xraw_command(netfn=0x32, command=0xa4, data=[bay, 2]) def get_diagnostic_data(self, savefile, progress=None, autosuffix=False, variant=None): if variant == 6: raise Exception('Service data not supported on FPC') rsp = self.ipmicmd.xraw_command(netfn=0x32, command=0xb1, data=[0]) if bytearray(rsp['data'])[0] != 0: raise Exception("Service data generation already in progress") rsp = self.ipmicmd.xraw_command(netfn=0x34, command=0x12, data=[0]) if bytearray(rsp['data'])[0] != 0: raise Exception("Service data generation already in progress") rsp['data'] = b'\x01' initpct = 1.0 if progress: progress({'phase': 'initializing', 'progress': initpct}) while bytearray(rsp['data'])[0] != 0: ipmisession.Session.pause(3) initpct += 3.0 if initpct > 99.0: initpct = 99.0 rsp = self.ipmicmd.xraw_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") 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.FileDownloader(wc, url, savefile) fd.start() while fd.isAlive(): fd.join(1) if progress and wc.get_download_progress(): progress({'phase': 'download', 'progress': 100 * wc.get_download_progress()}) if progress: progress({'phase': 'complete'}) return savefile def process_fru(self, fru): smmv1 = self.smm_variant & 0xf0 == 0 # TODO(jjohnson2): can also get EIOM, SMM, and riser data if warranted snum = bytes(self.ipmicmd.xraw_command( netfn=0x32, command=0xb0, data=(5, 1))['data'][:]) mnum = bytes(self.ipmicmd.xraw_command( netfn=0x32, command=0xb0, data=(5, 0))['data'][:]) if not smmv1: snum = snum[2:] mnum = mnum[2:] fru['Serial Number'] = snum.strip(b' \x00\xff').replace(b'\xff', b'') fru['Model'] = mnum.strip(b' \x00\xff').replace(b'\xff', b'') return fru def get_webclient(self): cv = self.ipmicmd.certverify wc = webclient.SecureHTTPConnection(self.smm, 443, verifycallback=cv) wc.vintage = util._monotonic_time() wc.connect() 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 = fromstring(authdata) for data in authdata.findall('authResult'): if int(data.text) != 0: raise Exception("Firmware update already in progress") for data in authdata.findall('forwardUrl'): if 'renew' in data.text: raise Exception("Account password has expired on remote " "device") wc.st1 = None wc.st2 = None for data in authdata.findall('st1'): wc.st1 = data.text for data in authdata.findall('st2'): 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() if not isinstance(indexhtml, str): indexhtml = indexhtml.decode('utf8') for line in indexhtml.split('\n'): if '"ST1"' in line: wc.st1 = line.split()[-1].replace( '"', '').replace(',', '') if '"ST2"' in line: 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) if not isinstance(body, str): body = body.decode('utf8') for line in body.split('\n'): if '"ST1"' in line: wc.st1 = line.split()[-1].replace( '"', '').replace(',', '') if '"ST2"' in line: wc.st2 = line.split()[-1].replace( '"', '').replace(',', '') if not wc.st2: raise Exception('Unable to locate ST2 token') wc.set_header('ST2', wc.st2) 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() def get_hostname(self): currinfo = self.get_netinfo() self.logout() 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) 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() def get_domain(self): currinfo = self.get_netinfo() self.logout() 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() 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() 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() 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()) srvs = [] for data in result.findall('ntpServer1'): srvs.append(data.text) for data in result.findall('ntpServer2'): srvs.append(data.text) for data in result.findall('ntpServer3'): srvs.append(data.text) self.logout() return srvs def update_firmware(self, filename, data=None, progress=None, bank=None): if progress is None: def progress(x): return True z = None if data and hasattr(data, 'read'): if zipfile.is_zipfile(data): z = zipfile.ZipFile(data) else: data.seek(0) elif data is None and zipfile.is_zipfile(filename): z = zipfile.ZipFile(filename) if z: for tmpname in z.namelist(): if tmpname.endswith('.rom'): filename = tmpname 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 = webclient.FileUploader( wc, url, filename, data, formname='fileUpload', otherfields={'preConfig': 'on'}) fu.start() while fu.isAlive(): fu.join(3) if progress: progress({'phase': 'upload', 'progress': 100 * wc.get_upload_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 while not complete: ipmisession.Session.pause(3) wc.request('POST', '/data', 'get=fwProgress,fwUpdate') rsp = wc.getresponse() progdata = rsp.read() 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) progress({'phase': 'apply', 'progress': percent}) complete = percent >= 100.0 return 'complete' def get_inventory_descriptions(self, ipmicmd, variant): if variant >> 5 == 0: return psucount = get_psu_count(ipmicmd, variant) for idx in range(psucount): yield 'PSU {}'.format(idx + 1) def get_inventory_of_component(self, ipmicmd, component): psuidx = int(component.replace('PSU ', '')) return self.get_psu_info(ipmicmd, psuidx) def get_psu_info(self, ipmicmd, psunum): psuinfo = ipmicmd.xraw_command(0x34, 0x6, data=(psunum,)) psuinfo = bytearray(psuinfo['data']) psutype = struct.unpack('