mirror of
https://opendev.org/x/pyghmi
synced 2026-05-13 18:04:19 +00:00
Implement server side IPMI protocol
Provide framework for a utility to listen and respond to ipmi protocol messages. Also provide an example 'fakebmc' to give a general idea of how to create a ipmi device Change-Id: I240b233ff161bc3672795b3ac3bf609e4c8c98bb
This commit is contained in:
+72
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python
|
||||
# Copyright 2015 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.
|
||||
__author__ = 'jjohnson2@lenovo.com'
|
||||
|
||||
#this is a quick sample of how to write something that acts like a bmc
|
||||
#to play:
|
||||
# run fakebmc
|
||||
## ipmitool -I lanplus -U admin -P password -H 127.0.0.1 power status
|
||||
# Chassis Power is off
|
||||
# # ipmitool -I lanplus -U admin -P password -H 127.0.0.1 power on
|
||||
# Chassis Power Control: Up/On
|
||||
# # ipmitool -I lanplus -U admin -P password -H 127.0.0.1 power status
|
||||
# Chassis Power is on
|
||||
# # ipmitool -I lanplus -U admin -P password -H 127.0.0.1 mc reset cold
|
||||
# Sent cold reset command to MC
|
||||
# (fakebmc exits)
|
||||
import pyghmi.ipmi.bmc as bmc
|
||||
import sys
|
||||
|
||||
|
||||
class FakeBmc(bmc.Bmc):
|
||||
def __init__(self, authdata):
|
||||
super(FakeBmc, self).__init__(authdata)
|
||||
self.powerstate = 'off'
|
||||
self.bootdevice = 'default'
|
||||
|
||||
def get_boot_device(self):
|
||||
return self.bootdevice
|
||||
|
||||
def set_boot_device(self, bootdevice):
|
||||
self.bootdevice = bootdevice
|
||||
|
||||
def cold_reset(self):
|
||||
# Reset of the BMC, not managed system, here we will exit the demo
|
||||
print 'shutting down in response to BMC cold reset request'
|
||||
sys.exit(0)
|
||||
|
||||
def get_power_state(self):
|
||||
return self.powerstate
|
||||
|
||||
def power_off(self):
|
||||
# this should be power down without waiting for clean shutdown
|
||||
self.powerstate = 'off'
|
||||
print 'abruptly remove power'
|
||||
|
||||
def power_on(self):
|
||||
self.powerstate = 'on'
|
||||
print 'powered on'
|
||||
|
||||
def power_reset(self):
|
||||
pass
|
||||
|
||||
def power_shutdown(self):
|
||||
# should attempt a clean shutdown
|
||||
print 'politely shut down the system'
|
||||
self.powerstate = 'off'
|
||||
|
||||
if __name__ == '__main__':
|
||||
mybmc = FakeBmc({'admin': 'password'})
|
||||
mybmc.listen()
|
||||
@@ -0,0 +1,147 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2015 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.
|
||||
|
||||
__author__ = 'jjohnson2@lenovo.com'
|
||||
|
||||
import pyghmi.ipmi.command as ipmicommand
|
||||
import pyghmi.ipmi.private.serversession as serversession
|
||||
import pyghmi.ipmi.private.session as ipmisession
|
||||
import traceback
|
||||
|
||||
|
||||
class Bmc(serversession.IpmiServer):
|
||||
def cold_reset(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def power_off(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def power_on(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def power_cycle(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def power_reset(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def power_shutdown(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_power_state(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def handle_missing_command(session):
|
||||
session.send_ipmi_response(code=0xc1)
|
||||
|
||||
def get_chassis_status(self, session):
|
||||
try:
|
||||
powerstate = self.get_power_state()
|
||||
except NotImplementedError:
|
||||
return session.send_ipmi_response(code=0xc1)
|
||||
if powerstate in ipmicommand.power_states:
|
||||
powerstate = ipmicommand.power_states[powerstate]
|
||||
if powerstate not in (0, 1):
|
||||
raise Exception('BMC implementation mistake')
|
||||
statusdata = [powerstate, 0, 0]
|
||||
session.send_ipmi_response(data=statusdata)
|
||||
|
||||
def control_chassis(self, request, session):
|
||||
rc = 0
|
||||
try:
|
||||
directive = request['data'][0]
|
||||
if directive == 0:
|
||||
rc = self.power_off()
|
||||
elif directive == 1:
|
||||
rc = self.power_on()
|
||||
elif directive == 2:
|
||||
rc = self.power_cycle()
|
||||
elif directive == 3:
|
||||
rc = self.power_reset()
|
||||
elif directive == 5:
|
||||
rc = self.power_shutdown()
|
||||
if rc is None:
|
||||
rc = 0
|
||||
session.send_ipmi_response(code=rc)
|
||||
except NotImplementedError:
|
||||
session.send_ipmi_response(code=0xcc)
|
||||
|
||||
def get_boot_device(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_system_boot_options(self, request, session):
|
||||
if request['data'][0] == 5: # boot flags
|
||||
try:
|
||||
bootdevice = self.get_boot_device()
|
||||
except NotImplementedError:
|
||||
session.send_ipmi_response(data=[1, 5, 0, 0, 0, 0, 0])
|
||||
if (type(bootdevice) != int and
|
||||
bootdevice in ipmicommand.boot_devices):
|
||||
bootdevice = ipmicommand.boot_devices[bootdevice]
|
||||
paramdata = [1, 5, 0b10000000, bootdevice, 0, 0, 0]
|
||||
return session.send_ipmi_response(data=paramdata)
|
||||
else:
|
||||
session.send_ipmi_response(code=0x80)
|
||||
|
||||
def set_boot_device(self, bootdevice):
|
||||
raise NotImplementedError
|
||||
|
||||
def set_system_boot_options(self, request, session):
|
||||
if request['data'][0] in (0, 3, 4):
|
||||
# for now, just smile and nod at boot flag bit clearing
|
||||
# implementing it is a burden and implementing it does more to
|
||||
# confuse users than serve a useful purpose
|
||||
session.send_ipmi_response()
|
||||
elif request['data'][0] == 5:
|
||||
bootdevice = (request['data'][2] >> 2) & 0b1111
|
||||
try:
|
||||
bootdevice = ipmicommand.boot_devices[bootdevice]
|
||||
except KeyError:
|
||||
session.send_ipmi_response(code=0xcc)
|
||||
return
|
||||
self.set_boot_device(bootdevice)
|
||||
session.send_ipmi_response()
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
def handle_raw_request(self, request, session):
|
||||
try:
|
||||
if request['netfn'] == 6:
|
||||
if request['command'] == 1: # get device id
|
||||
return self.send_device_id(session)
|
||||
elif request['command'] == 2: # cold reset
|
||||
return session.send_ipmi_response(code=self.cold_reset())
|
||||
elif request['netfn'] == 0:
|
||||
if request['command'] == 1: # get chassis status
|
||||
return self.get_chassis_status(session)
|
||||
elif request['command'] == 2: # chassis control
|
||||
return self.control_chassis(request, session)
|
||||
elif request['command'] == 8: # set boot options
|
||||
return self.set_system_boot_options(request, session)
|
||||
elif request['command'] == 9: # get boot options
|
||||
return self.get_system_boot_options(request, session)
|
||||
session.send_ipmi_response(code=0xc1)
|
||||
except NotImplementedError:
|
||||
session.send_ipmi_response(code=0xc1)
|
||||
except Exception:
|
||||
session._send_ipmi_net_payload(code=0xff)
|
||||
traceback.print_exc()
|
||||
|
||||
@classmethod
|
||||
def listen(cls):
|
||||
while True:
|
||||
ipmisession.Session.wait_for_rsp(30)
|
||||
@@ -42,6 +42,7 @@ boot_devices = {
|
||||
3: 'safe',
|
||||
5: 'optical',
|
||||
6: 'setup',
|
||||
15: 'floppy',
|
||||
0: 'default'
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2015 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.
|
||||
|
||||
# This represents the server side of a session object
|
||||
# Split into a separate file to avoid overly manipulating the as-yet
|
||||
# client-centered session object
|
||||
import hashlib
|
||||
import hmac
|
||||
import os
|
||||
import pyghmi.ipmi.private.constants as constants
|
||||
import pyghmi.ipmi.private.session as ipmisession
|
||||
import struct
|
||||
import uuid
|
||||
|
||||
|
||||
class ServerSession(ipmisession.Session):
|
||||
def __new__(cls, authdata, kg, clientaddr, netsocket, request, uuid,
|
||||
bmc):
|
||||
# Need to do default new type behavior. The normal session
|
||||
# takes measures to assure the caller shares even when they
|
||||
# didn't try. We don't have that operational mode to contend
|
||||
# with in the server case (one file descriptor per bmc)
|
||||
return object.__new__(cls)
|
||||
|
||||
def create_open_session_response(self, request):
|
||||
clienttag = ord(request[0])
|
||||
# role = request[1]
|
||||
self.clientsessionid = list(struct.unpack('4B', request[4:8]))
|
||||
# TODO(jbjohnso): intelligently handle integrity/auth/conf
|
||||
#for now, forcibly do cipher suite 3
|
||||
self.managedsessionid = list(struct.unpack('4B', os.urandom(4)))
|
||||
#table 13-17, 1 for now (hmac-sha1), 3 should also be supported
|
||||
#table 13-18, integrity, 1 for now is hmac-sha1-96, 4 is sha256
|
||||
#confidentiality: 1 is aes-cbc-128, the only one
|
||||
self.privlevel = 4
|
||||
response = ([clienttag, 0, self.privlevel, 0] +
|
||||
self.clientsessionid + self.managedsessionid +
|
||||
[
|
||||
0, 0, 0, 8, 1, 0, 0, 0, # auth
|
||||
1, 0, 0, 8, 1, 0, 0, 0, # integrity
|
||||
2, 0, 0, 8, 1, 0, 0, 0, # privacy
|
||||
])
|
||||
return response
|
||||
|
||||
def __init__(self, authdata, kg, clientaddr, netsocket, request, uuid,
|
||||
bmc):
|
||||
# begin conversation per RMCP+ open session request
|
||||
self.uuid = uuid
|
||||
self.rqaddr = constants.IPMI_BMC_ADDRESS
|
||||
self.authdata = authdata
|
||||
self.servermode = True
|
||||
self.ipmiversion = 2.0
|
||||
self.sequencenumber = 0
|
||||
self.sessionid = 0
|
||||
self.bmc = bmc
|
||||
self.lastpayload = None
|
||||
self.broken = False
|
||||
self.authtype = 6
|
||||
self.integrityalgo = 0
|
||||
self.confalgo = 0
|
||||
self.kg = kg
|
||||
self.socket = netsocket
|
||||
self.sockaddr = clientaddr
|
||||
ipmisession.Session.bmc_handlers[clientaddr] = self
|
||||
response = self.create_open_session_response(request)
|
||||
self.send_payload(response,
|
||||
constants.payload_types['rmcpplusopenresponse'],
|
||||
retry=False)
|
||||
|
||||
def _got_rmcp_openrequest(self, data):
|
||||
response = self.create_open_session_response(
|
||||
struct.pack('B' * len(data), *data))
|
||||
self.send_payload(response,
|
||||
constants.payload_types['rmcpplusopenresponse'],
|
||||
retry=False)
|
||||
|
||||
def _got_rakp1(self, data):
|
||||
clienttag = data[0]
|
||||
self.Rm = data[8:24]
|
||||
self.rolem = data[24]
|
||||
self.maxpriv = self.rolem & 0b111
|
||||
namepresent = data[27]
|
||||
if namepresent == 0:
|
||||
#ignore null username for now
|
||||
return
|
||||
usernamebytes = data[28:]
|
||||
self.username = struct.pack('%dB' % len(usernamebytes), *usernamebytes)
|
||||
if self.username not in self.authdata:
|
||||
# don't think about invalid usernames for now
|
||||
return
|
||||
uuidbytes = self.uuid.bytes
|
||||
uuidbytes = list(struct.unpack('%dB' % len(uuidbytes), uuidbytes))
|
||||
self.uuiddata = uuidbytes
|
||||
self.Rc = list(struct.unpack('16B', os.urandom(16)))
|
||||
hmacdata = (self.clientsessionid + self.managedsessionid +
|
||||
self.Rm + self.Rc + uuidbytes +
|
||||
[self.rolem, len(self.username)])
|
||||
hmacdata = struct.pack('%dB' % len(hmacdata), *hmacdata)
|
||||
hmacdata += self.username
|
||||
self.kuid = self.authdata[self.username]
|
||||
if self.kg is None:
|
||||
self.kg = self.kuid
|
||||
authcode = hmac.new(
|
||||
self.kuid, hmacdata, hashlib.sha1).digest()
|
||||
authcode = list(struct.unpack('%dB' % len(authcode), authcode))
|
||||
# regretably, ipmi mandates the server send out an hmac first
|
||||
# akin to a leak of /etc/shadow, not too worrisome if the secret
|
||||
# is complex, but terrible for most likely passwords selected by
|
||||
# a human
|
||||
newmessage = ([clienttag, 0, 0, 0] + self.clientsessionid +
|
||||
self.Rc + uuidbytes + authcode)
|
||||
self.send_payload(newmessage, constants.payload_types['rakp2'],
|
||||
retry=False)
|
||||
|
||||
def _got_rakp2(self, data):
|
||||
# stub, server should not think about rakp2
|
||||
pass
|
||||
|
||||
def _got_rakp3(self, data):
|
||||
# for now drop rakp3 with bad authcode
|
||||
# respond correctly a TODO(jjohnson2), since Kg being used
|
||||
# yet incorrect is a scenario why rakp3 could be bad
|
||||
# even if rakp2 was good
|
||||
RmRc = struct.pack('B' * len(self.Rm + self.Rc), *(self.Rm + self.Rc))
|
||||
self.sik = hmac.new(self.kg,
|
||||
RmRc +
|
||||
struct.pack("2B", self.rolem,
|
||||
len(self.username)) +
|
||||
self.username, hashlib.sha1).digest()
|
||||
self.k1 = hmac.new(self.sik, '\x01' * 20, hashlib.sha1).digest()
|
||||
self.k2 = hmac.new(self.sik, '\x02' * 20, hashlib.sha1).digest()
|
||||
self.aeskey = self.k2[0:16]
|
||||
hmacdata = struct.pack('B' * len(self.Rc), *self.Rc) +\
|
||||
struct.pack("4B", *self.clientsessionid) +\
|
||||
struct.pack("2B", self.rolem,
|
||||
len(self.username)) +\
|
||||
self.username
|
||||
expectedauthcode = hmac.new(self.kuid, hmacdata, hashlib.sha1).digest()
|
||||
authcode = struct.pack("%dB" % len(data[8:]), *data[8:])
|
||||
if expectedauthcode != authcode:
|
||||
#TODO(jjohnson2): RMCP error back at invalid rakp3
|
||||
return
|
||||
clienttag = data[0]
|
||||
if data[1] != 0:
|
||||
# client did not like our response, so ignore the rakp3
|
||||
return
|
||||
self.localsid = struct.unpack('<I',
|
||||
struct.pack(
|
||||
'4B', *self.managedsessionid))[0]
|
||||
self.ipmicallback = self.handle_client_request
|
||||
self._send_rakp4(clienttag, 0)
|
||||
|
||||
def handle_client_request(self, request):
|
||||
if request['netfn'] == 6 and request['command'] == 0x3b:
|
||||
pendingpriv = request['data'][0]
|
||||
returncode = 0
|
||||
if pendingpriv > 1:
|
||||
if pendingpriv > self.maxpriv:
|
||||
returncode = 0x81
|
||||
else:
|
||||
self.clientpriv = request['data'][0]
|
||||
self._send_ipmi_net_payload(code=returncode,
|
||||
data=[self.clientpriv])
|
||||
elif request['netfn'] == 6 and request['command'] == 0x3c:
|
||||
self.send_ipmi_response()
|
||||
self.close_server_session()
|
||||
else:
|
||||
self.bmc.handle_raw_request(request, self)
|
||||
|
||||
def close_server_session(self):
|
||||
pass
|
||||
|
||||
def _send_rakp4(self, tagvalue, statuscode):
|
||||
payload = [tagvalue, statuscode, 0, 0] + self.clientsessionid
|
||||
hmacdata = self.Rm + self.managedsessionid + self.uuiddata
|
||||
hmacdata = struct.pack('%dB' % len(hmacdata), *hmacdata)
|
||||
authdata = hmac.new(self.sik, hmacdata, hashlib.sha1).digest()[:12]
|
||||
payload += struct.unpack('%dB' % len(authdata), authdata)
|
||||
self.send_payload(payload, constants.payload_types['rakp4'],
|
||||
retry=False)
|
||||
self.confalgo = 'aes'
|
||||
self.integrityalgo = 'sha1'
|
||||
self.sessionid = struct.unpack(
|
||||
'<I', struct.pack('4B', *self.clientsessionid))[0]
|
||||
|
||||
def _got_rakp4(self, data):
|
||||
# stub, server should not think about rakp4
|
||||
pass
|
||||
|
||||
def _timedout(self):
|
||||
"""Expire a client session after a period of inactivity
|
||||
|
||||
After the session inactivity timeout, this invalidate the client
|
||||
session.
|
||||
"""
|
||||
#for now, we will have a non-configurable 60 second timeout
|
||||
pass
|
||||
|
||||
def _handle_channel_auth_cap(self, request):
|
||||
"""Handle incoming channel authentication capabilities request
|
||||
|
||||
This is used when serving as an IPMI target to service client
|
||||
requests for client authentication capabilities
|
||||
"""
|
||||
pass
|
||||
|
||||
def send_ipmi_response(self, data=[], code=0):
|
||||
self._send_ipmi_net_payload(data=data, code=code)
|
||||
|
||||
def logout(self):
|
||||
pass
|
||||
|
||||
|
||||
class IpmiServer(object):
|
||||
#auth capabilities for now is a static payload
|
||||
#for now always completion code 0, otherwise ignore
|
||||
#authentication type fixed to ipmi2, ipmi1 forbidden
|
||||
# 0b10000000
|
||||
|
||||
def __init__(self, authdata, port=623, bmcuuid=None):
|
||||
"""Create a new ipmi bmc instance.
|
||||
|
||||
:param authdata: A dict or object with .get() to provide password
|
||||
lookup by username. This does not support the full
|
||||
complexity of what IPMI can support, only a
|
||||
reasonable subset.
|
||||
:param port: The default port number to bind to. Defaults to the
|
||||
standard 623
|
||||
"""
|
||||
self.revision = 0
|
||||
self.deviceid = 0
|
||||
self.firmwaremajor = 1
|
||||
self.firmwareminor = 0
|
||||
self.ipmiversion = 2
|
||||
self.additionaldevices = 0
|
||||
self.mfgid = 0
|
||||
self.prodid = 0
|
||||
if bmcuuid is None:
|
||||
self.uuid = uuid.uuid4()
|
||||
else:
|
||||
self.uuid = bmcuuid
|
||||
lanchannel = 1
|
||||
authtype = 0b10000000 # ipmi2 only
|
||||
authstatus = 0b00000100 # change based on authdata/kg
|
||||
chancap = 0b00000010 # ipmi2 only
|
||||
oemdata = (0, 0, 0, 0)
|
||||
self.authdata = authdata
|
||||
self.authcap = struct.pack('BBBBBBBBB', 0, lanchannel, authtype,
|
||||
authstatus, chancap, *oemdata)
|
||||
self.kg = None
|
||||
self.timeout = 60
|
||||
self.serversocket = ipmisession.Session._assignsocket(
|
||||
('::', port, 0, 0))
|
||||
ipmisession.Session.bmc_handlers[self.serversocket] = self
|
||||
|
||||
def send_auth_cap(self, myaddr, mylun, clientaddr, clientlun, sockaddr):
|
||||
header = '\x06\x00\xff\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10'
|
||||
headerdata = (clientaddr, clientlun | (7 << 2))
|
||||
headersum = ipmisession._checksum(*headerdata)
|
||||
header += struct.pack('BBBBBB',
|
||||
*(headerdata + (headersum, myaddr, mylun, 0x38)))
|
||||
header += self.authcap
|
||||
bodydata = struct.unpack('B' * len(header[17:]), header[17:])
|
||||
header += chr(ipmisession._checksum(*bodydata))
|
||||
ipmisession._io_sendto(self.serversocket, header, sockaddr)
|
||||
|
||||
def sessionless_data(self, data, sockaddr):
|
||||
"""Examines unsolocited packet and decides appropriate action.
|
||||
|
||||
For a listening IpmiServer, a packet without an active session
|
||||
comes here for examination. If it is something that is utterly
|
||||
sessionless (e.g. get channel authentication), send the appropriate
|
||||
response. If it is a get session challenge or open rmcp+ request,
|
||||
spawn a session to handle the context.
|
||||
"""
|
||||
if len(data) < 22:
|
||||
return
|
||||
if not (data[0] == '\x06' and data[2:4] == '\xff\x07'): # not ipmi
|
||||
return
|
||||
if data[4] == '\x06': # ipmi 2 payload...
|
||||
payloadtype = data[5]
|
||||
if payloadtype not in ('\x00', '\x10'):
|
||||
return
|
||||
if payloadtype == '\x10': # new session to handle conversation
|
||||
ServerSession(self.authdata, self.kg, sockaddr,
|
||||
self.serversocket, data[16:], self.uuid,
|
||||
bmc=self)
|
||||
return
|
||||
data = data[13:] # ditch 13 bytes so the payload works out
|
||||
myaddr, netfnlun = struct.unpack('2B', data[14:16])
|
||||
netfn = (netfnlun & 0b11111100) >> 2
|
||||
mylun = netfnlun & 0b11
|
||||
if netfn == 6: # application request
|
||||
if data[19] == '\x38': # cmd = get channel auth capabilities
|
||||
verchannel, level = struct.unpack('2B', data[20:22])
|
||||
version = verchannel & 0b10000000
|
||||
if version != 0b10000000:
|
||||
return
|
||||
channel = verchannel & 0b1111
|
||||
if channel != 0xe:
|
||||
return
|
||||
(clientaddr, clientlun) = struct.unpack('BB', data[17:19])
|
||||
level &= 0b1111
|
||||
self.send_auth_cap(myaddr, mylun, clientaddr, clientlun,
|
||||
sockaddr)
|
||||
|
||||
def set_kg(self, kg):
|
||||
"""Sets the Kg for the BMC to use
|
||||
|
||||
In RAKP, Kg is a BMC-specific integrity key that can be set. If not
|
||||
set, Kuid is used for the integrity key
|
||||
"""
|
||||
try:
|
||||
self.kg = kg.encode('utf-8')
|
||||
except AttributeError:
|
||||
self.kg = kg
|
||||
|
||||
def send_device_id(self, session):
|
||||
response = [self.deviceid, self.revision, self.firmwaremajor,
|
||||
self.firmwareminor, self.ipmiversion,
|
||||
self.additionaldevices]
|
||||
response += struct.unpack('4B', struct.pack('<I', self.mfgid))
|
||||
response += struct.unpack('4B', struct.pack('<I', self.prodid))
|
||||
session.send_ipmi_response(data=response)
|
||||
|
||||
def handle_raw_request(self, request, session):
|
||||
# per table 5-2, completion code 0xc1 is 'unrecognized'
|
||||
session.send_ipmi_response(code=0xc1)
|
||||
|
||||
def logout(self):
|
||||
pass
|
||||
@@ -1,6 +1,7 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2013 IBM Corporation
|
||||
# Copyright 2015 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -269,12 +270,13 @@ class Session(object):
|
||||
def _initsessions(cls):
|
||||
atexit.register(cls._cleanup)
|
||||
|
||||
def _assignsocket(self):
|
||||
@classmethod
|
||||
def _assignsocket(cls, server=None):
|
||||
global iothread
|
||||
global iothreadready
|
||||
global iosockets
|
||||
if iothread is None:
|
||||
self._initsessions()
|
||||
cls._initsessions()
|
||||
initevt = threading.Event()
|
||||
iothreadwaiters.append(initevt)
|
||||
iothread = threading.Thread(target=_ioworker)
|
||||
@@ -288,17 +290,22 @@ class Session(object):
|
||||
# seek for the least used socket. As sessions close, they may free
|
||||
# up slots in seemingly 'full' sockets. This scheme allows those
|
||||
# slots to be recycled
|
||||
sorted_candidates = sorted(self.socketpool.iteritems(),
|
||||
key=operator.itemgetter(1))
|
||||
sorted_candidates = None
|
||||
if server is None:
|
||||
sorted_candidates = sorted(cls.socketpool.iteritems(),
|
||||
key=operator.itemgetter(1))
|
||||
if sorted_candidates and sorted_candidates[0][1] < MAX_BMCS_PER_SOCKET:
|
||||
self.socketpool[sorted_candidates[0][0]] += 1
|
||||
cls.socketpool[sorted_candidates[0][0]] += 1
|
||||
return sorted_candidates[0][0]
|
||||
# we need a new socket
|
||||
tmpsocket = _io_apply(socket.socket,
|
||||
(socket.AF_INET6, socket.SOCK_DGRAM)) # INET6
|
||||
# can do IPv4 if you are nice to it
|
||||
tmpsocket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
|
||||
self.socketpool[tmpsocket] = 1
|
||||
if server is None:
|
||||
cls.socketpool[tmpsocket] = 1
|
||||
else:
|
||||
tmpsocket.bind(server)
|
||||
iosockets.append(tmpsocket)
|
||||
return tmpsocket
|
||||
|
||||
@@ -356,6 +363,7 @@ class Session(object):
|
||||
self.maxtimeout = 3 # be aggressive about giving up on initial packet
|
||||
self.incommand = False
|
||||
self.nameonly = 16 # default to name only lookups in RAKP exchange
|
||||
self.servermode = False
|
||||
self.initialized = True
|
||||
self.cleaningup = False
|
||||
self.lastpayload = None
|
||||
@@ -507,7 +515,8 @@ class Session(object):
|
||||
# we are always the requestor for now
|
||||
seqincrement = 7 # IPMI spec forbids gaps bigger then 7 in seq number.
|
||||
# Risk the taboo rather than violate the rules
|
||||
while ((netfn, command, self.seqlun) in self.tabooseq and
|
||||
while (not self.servermode and
|
||||
(netfn, command, self.seqlun) in self.tabooseq and
|
||||
self.tabooseq[(netfn, command, self.seqlun)] and seqincrement):
|
||||
self.tabooseq[(self.expectednetfn, command, self.seqlun)] -= 1
|
||||
# Allow taboo to eventually expire after a few rounds
|
||||
@@ -526,7 +535,8 @@ class Session(object):
|
||||
else:
|
||||
rqaddr = self.rqaddr
|
||||
rsaddr = constants.IPMI_BMC_ADDRESS
|
||||
|
||||
if self.servermode:
|
||||
rsaddr = self.clientaddr
|
||||
#figure 13-4, first two bytes are rsaddr and
|
||||
# netfn, for non-bridge request, rsaddr is always 0x20 since we are
|
||||
# addressing BMC while rsaddr is specified forbridge request
|
||||
@@ -542,7 +552,8 @@ class Session(object):
|
||||
tail_csum = _checksum(*payload[3:])
|
||||
payload.append(tail_csum)
|
||||
|
||||
self._add_request_entry((self.expectednetfn, self.seqlun, command))
|
||||
if not self.servermode:
|
||||
self._add_request_entry((self.expectednetfn, self.seqlun, command))
|
||||
return payload
|
||||
|
||||
def _generic_callback(self, response):
|
||||
@@ -602,8 +613,17 @@ class Session(object):
|
||||
self.incommand = False
|
||||
return lastresponse
|
||||
|
||||
def _send_ipmi_net_payload(self, netfn, command, data, bridge_request=None,
|
||||
retry=True, delay_xmit=None):
|
||||
def _send_ipmi_net_payload(self, netfn=None, command=None, data=[], code=0,
|
||||
bridge_request=None,
|
||||
retry=None, delay_xmit=None):
|
||||
if retry is None:
|
||||
retry = not self.servermode
|
||||
if self.servermode:
|
||||
data = [code] + data
|
||||
if netfn is None:
|
||||
netfn = self.clientnetfn
|
||||
if command is None:
|
||||
command = self.clientcommand
|
||||
ipmipayload = self._make_ipmi_payload(netfn, command, bridge_request,
|
||||
data)
|
||||
payload_type = constants.payload_types['ipmi']
|
||||
@@ -889,6 +909,7 @@ class Session(object):
|
||||
rdata = _io_apply(_io_recvfrom, (mysocket, 3000))
|
||||
if rdata is None:
|
||||
break
|
||||
rdata = rdata + (mysocket,)
|
||||
queue.append(rdata)
|
||||
|
||||
@classmethod
|
||||
@@ -953,8 +974,8 @@ class Session(object):
|
||||
pktqueue = collections.deque([])
|
||||
cls.pulltoqueue(iosockets, pktqueue)
|
||||
while len(pktqueue):
|
||||
(data, sockaddr) = pktqueue.popleft()
|
||||
cls._route_ipmiresponse(sockaddr, data)
|
||||
(data, sockaddr, mysocket) = pktqueue.popleft()
|
||||
cls._route_ipmiresponse(sockaddr, data, mysocket)
|
||||
cls.pulltoqueue(iosockets, pktqueue)
|
||||
sessionstodel = []
|
||||
sessionstokeepalive = []
|
||||
@@ -1036,14 +1057,16 @@ class Session(object):
|
||||
self._mark_broken()
|
||||
|
||||
@classmethod
|
||||
def _route_ipmiresponse(cls, sockaddr, data):
|
||||
def _route_ipmiresponse(cls, sockaddr, data, mysocket):
|
||||
if not (data[0] == '\x06' and data[2:4] == '\xff\x07'): # not ipmi
|
||||
return
|
||||
try:
|
||||
cls.bmc_handlers[sockaddr]._handle_ipmi_packet(data,
|
||||
sockaddr=sockaddr)
|
||||
except KeyError:
|
||||
pass
|
||||
# check if we have a server attached to the target socket
|
||||
if mysocket in cls.bmc_handlers:
|
||||
cls.bmc_handlers[mysocket].sessionless_data(data, sockaddr)
|
||||
|
||||
def _handle_ipmi_packet(self, data, sockaddr=None):
|
||||
if self.sockaddr is None and sockaddr is not None:
|
||||
@@ -1089,16 +1112,33 @@ class Session(object):
|
||||
else:
|
||||
return # unrecognized data, assume evil
|
||||
|
||||
def _got_rakp1(self, data):
|
||||
# stub, client sessions ignore rakp2
|
||||
pass
|
||||
|
||||
def _got_rakp3(self, data):
|
||||
#stub, client sessions ignore rakp3
|
||||
pass
|
||||
|
||||
def _got_rmcp_openrequest(self, data):
|
||||
pass
|
||||
|
||||
def _handle_ipmi2_packet(self, rawdata):
|
||||
data = list(struct.unpack("%dB" % len(rawdata), rawdata))
|
||||
#now need mutable array
|
||||
ptype = data[5] & 0b00111111
|
||||
# the first 16 bytes are header information as can be seen in 13-8 that
|
||||
# we will toss out
|
||||
if ptype == 0x11: # rmcp+ response
|
||||
if ptype == 0x10:
|
||||
return self._got_rmcp_openrequest(data[16:])
|
||||
elif ptype == 0x11: # rmcp+ response
|
||||
return self._got_rmcp_response(data[16:])
|
||||
elif ptype == 0x12:
|
||||
return self._got_rakp1(data[16:])
|
||||
elif ptype == 0x13:
|
||||
return self._got_rakp2(data[16:])
|
||||
elif ptype == 0x14:
|
||||
return self._got_rakp3(data[16:])
|
||||
elif ptype == 0x15:
|
||||
return self._got_rakp4(data[16:])
|
||||
elif ptype == 0 or ptype == 1: # good old ipmi payload or sol
|
||||
@@ -1265,6 +1305,7 @@ class Session(object):
|
||||
struct.pack("2B", self.nameonly | self.privlevel,
|
||||
len(self.userid)) +\
|
||||
self.userid
|
||||
|
||||
authcode = hmac.new(self.password, hmacdata, hashlib.sha1).digest()
|
||||
payload += list(struct.unpack("%dB" % len(authcode), authcode))
|
||||
self.send_payload(
|
||||
@@ -1318,6 +1359,13 @@ class Session(object):
|
||||
# For now, skip the checksums since we are in LAN only,
|
||||
# TODO(jbjohnso): if implementing other channels, add checksum checks
|
||||
# here
|
||||
if self.servermode:
|
||||
self.seqlun = payload[4]
|
||||
self.clientaddr = payload[3]
|
||||
self.clientnetfn = (payload[1] >> 2) + 1
|
||||
self.clientcommand = payload[5]
|
||||
self._parse_payload(payload)
|
||||
return
|
||||
entry = (payload[1] >> 2, payload[4], payload[5])
|
||||
if self._lookup_request_entry(entry):
|
||||
self._remove_request_entry(entry)
|
||||
@@ -1353,8 +1401,9 @@ class Session(object):
|
||||
self.expectednetfn = 0x1ff # bigger than one byte means it can never
|
||||
# match the one byte value by mistake
|
||||
self.expectedcmd = 0x1ff
|
||||
self.seqlun += 4 # prepare seqlun for next transmit
|
||||
self.seqlun &= 0xff # when overflowing, wrap around
|
||||
if not self.servermode:
|
||||
self.seqlun += 4 # prepare seqlun for next transmit
|
||||
self.seqlun &= 0xff # when overflowing, wrap around
|
||||
Session.waiting_sessions.pop(self, None)
|
||||
self.lastpayload = None # render retry mechanism utterly incapable of
|
||||
# doing anything, though it shouldn't matter
|
||||
@@ -1365,11 +1414,15 @@ class Session(object):
|
||||
# ^^ remove header of rsaddr/netfn/lun/checksum/rq/seq/lun
|
||||
del payload[-1] # remove the trailing checksum
|
||||
response['command'] = payload[0]
|
||||
response['code'] = payload[1]
|
||||
del payload[0:2]
|
||||
response['data'] = payload
|
||||
if self.servermode:
|
||||
del payload[0:1]
|
||||
response['data'] = payload
|
||||
else:
|
||||
response['code'] = payload[1]
|
||||
del payload[0:2]
|
||||
response['data'] = payload
|
||||
self.timeout = initialtimeout + (0.5 * random.random())
|
||||
if len(self.pendingpayloads) > 0:
|
||||
if not self.servermode and len(self.pendingpayloads) > 0:
|
||||
(nextpayload, nextpayloadtype, retry) = \
|
||||
self.pendingpayloads.popleft()
|
||||
self.send_payload(payload=nextpayload,
|
||||
|
||||
Reference in New Issue
Block a user