mirror of
https://opendev.org/x/pyghmi
synced 2025-02-20 12:30:48 +00:00
Milestone: can now receive and correctly parse get channel auth capabilities result
This commit is contained in:
parent
6b575b84d2
commit
9269810e7c
190
ipmibase.py
190
ipmibase.py
@ -3,6 +3,9 @@
|
||||
import select
|
||||
import Crypto
|
||||
import socket
|
||||
from collections import deque
|
||||
from time import time
|
||||
from hashlib import md5
|
||||
from struct import pack, unpack
|
||||
from ipmi_constants import payload_types
|
||||
from random import random
|
||||
@ -12,24 +15,27 @@ initialtimeout = 0.5 #minimum timeout for first packet to retry in any given ses
|
||||
class IPMISession:
|
||||
poller=select.poll()
|
||||
bmc_handlers={}
|
||||
sessions_waiting={}
|
||||
waiting_sessions={}
|
||||
peeraddr_to_nodes={}
|
||||
def _createsocket(self):
|
||||
IPMISession.socket = socket.socket(socket.AF_INET6,socket.SOCK_DGRAM) #INET6 can do IPv4 if you are nice to it
|
||||
@classmethod
|
||||
def _createsocket(cls):
|
||||
cls.socket = socket.socket(socket.AF_INET6,socket.SOCK_DGRAM) #INET6 can do IPv4 if you are nice to it
|
||||
try: #we will try to fixup our receive buffer size if we are smaller than allowed.
|
||||
maxmf = open("/proc/sys/net/core/rmem_max")
|
||||
rmemmax = int(maxmf.read())
|
||||
rmemmax = rmemmax/2
|
||||
curmax=IPMISession.socket.getsockopt(socket.SOL_SOCKET,socket.SO_RCVBUF)
|
||||
curmax=cls.socket.getsockopt(socket.SOL_SOCKET,socket.SO_RCVBUF)
|
||||
curmax = curmax/2
|
||||
if (rmemmax > curmax):
|
||||
IPMISession.socket.setsockopt(socket.SOL_SOCKET,socket.SO_RCVBUF,rmemmax)
|
||||
cls.socket.setsockopt(socket.SOL_SOCKET,socket.SO_RCVBUF,rmemmax)
|
||||
except:
|
||||
pass
|
||||
curmax=IPMISession.socket.getsockopt(socket.SOL_SOCKET,socket.SO_RCVBUF)
|
||||
curmax=cls.socket.getsockopt(socket.SOL_SOCKET,socket.SO_RCVBUF)
|
||||
cls.poller.register(cls.socket,select.POLLIN)
|
||||
curmax = curmax/2
|
||||
#we throttle such that we never have no more outstanding packets than our receive buffer should be able to handle
|
||||
IPMISession.maxpending=curmax/1000 #pessimistically assume 1 kilobyte messages, way larger than almost all ipmi datagrams
|
||||
cls.pending=0
|
||||
cls.maxpending=curmax/1000 #pessimistically assume 1 kilobyte messages, way larger than almost all ipmi datagrams
|
||||
#for faster performance, sysadmins may want to examine and tune /proc/sys/net/core/rmem_max up. This allows the module to request more,
|
||||
#but does not increase buffers for applications that do less creative things
|
||||
#TODO: perhaps spread sessions across a socket pool when rmem_max is small, still get ~65/socket, but avoid long queues that might happen with
|
||||
@ -55,7 +61,7 @@ class IPMISession:
|
||||
self.sockaddr=None #when we confirm a working sockaddr, put it here to skip getaddrinfo
|
||||
self.tabooseq={} #this tracks netfn,command,seqlun combinations that were retried so that
|
||||
#we don't loop around and reuse the same request data and cause potential ambiguity in return
|
||||
self.ipmi15only=0 #default to supporting ipmi 2.0. Strictly by spec, this should gracefully be backwards compat, but some 1.5 implementations checked reserved bits
|
||||
self.ipmi15only=1 #default to supporting ipmi 2.0. Strictly by spec, this should gracefully be backwards compat, but some 1.5 implementations checked reserved bits
|
||||
def _checksum(self,*data): #Two's complement over the data
|
||||
csum=sum(data)
|
||||
csum=csum^0xff
|
||||
@ -68,10 +74,11 @@ class IPMISession:
|
||||
'''
|
||||
def _make_ipmi_payload(self,netfn,command,data=()):
|
||||
self.expectedcmd=command
|
||||
self.expectednetfn=netfn
|
||||
self.expectednetfn=netfn+1 #in ipmi, the response netfn is always one higher than the request payload, we assume 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 self.tabooseq[(netfn,command,self.seqlun)] and seqincrement:
|
||||
self.tabooseq[(netfn,command,self.seqlun)]-=1 #Allow taboo to eventually expire after a few rounds
|
||||
self.tabooseq[(self.expectednetfn,command,self.seqlun)]-=1 #Allow taboo to eventually expire after a few rounds
|
||||
self.seqlun += 4 #the last two bits are lun, so add 4 to add 1
|
||||
self.seqlun &= 0xff #we only have one byte, wrap when exceeded
|
||||
seqincrement-=1
|
||||
@ -89,37 +96,53 @@ class IPMISession:
|
||||
payload_type |= 0b01000000
|
||||
if hasattr(self,"confidentiality_algorithm"):
|
||||
payload_type |= 0b10000000
|
||||
self._pack_payload(payload=ipmipayload,type=payload_type)
|
||||
def _pack_payload(self,payload=None,type=None):
|
||||
self._pack_payload(payload=ipmipayload,payload_type=payload_type)
|
||||
def _pack_payload(self,payload=None,payload_type=None):
|
||||
if payload is None:
|
||||
payload=self.lastpayload
|
||||
if type is None:
|
||||
type=self.lasttype
|
||||
if payload_type is None:
|
||||
payload_type=self.last_payload_type
|
||||
message = [0x6,0,0xff,0x07] #constant RMCP header for IPMI
|
||||
baretype = type & 0b00111111
|
||||
baretype = payload_type & 0b00111111
|
||||
self.lastpayload=payload
|
||||
self.lasttype=type
|
||||
self.last_payload_type=payload_type
|
||||
message.append(self.authtype)
|
||||
if (self.ipmiversion == 2.0):
|
||||
message.append(type)
|
||||
if (type == 2):
|
||||
message.append(payload_type)
|
||||
if (payload_type == 2):
|
||||
pass #TODO: OEM payloads, currently not supported
|
||||
message += unpack("!4B",pack("!I",self.sessionid))
|
||||
message += unpack("!4B",pack("!I",self.sequencenumber))
|
||||
if (self.ipmiversion == 1.5):
|
||||
message += unpack("!4B",pack("!I",self.sessionid))
|
||||
if not self.authtype == 0:
|
||||
self._ipmi15authcode(payload)
|
||||
message += self._ipmi15authcode(payload)
|
||||
message.append(len(payload))
|
||||
message += payload
|
||||
totlen=34+len(message) #Guessing the ipmi spec means the whole packet ande assume no tag in old 1.5 world
|
||||
if (totlen in (56,84,112,128,156)):
|
||||
message.append(0) #Legacy pad as mandated by ipmi spec
|
||||
elif self.ipmiversion == 2.0:
|
||||
pass
|
||||
#TODO: ipmi 2.0
|
||||
self.netpacket = pack("!%dB"%len(message),*message)
|
||||
self._xmit_packet()
|
||||
|
||||
def _ipmi15authcode(self,*payload):
|
||||
pass #TODO
|
||||
def _ipmi15authcode(self,*payload,**kwargs):
|
||||
if self.authtype == 0: #Only for things prior to auth in ipmi 1.5, not like 2.0 cipher suite 0
|
||||
return ()
|
||||
passdata = unpack("%dB"%len(self.password),self.password)
|
||||
if 'checkremotecode' in kwargs and kwargs['checkremotecode']:
|
||||
seqbytes = unpack("!4B",pack("!I",self.remotesequencenumber))
|
||||
else:
|
||||
seqbytes = unpack("!4B",pack("!I",self.sequencenumber))
|
||||
sessdata = unpack("!4B",pack("!I",self.sessionid))
|
||||
bodydata = passdata + sessdata + payload + seqbytes + passdata
|
||||
dgst = md5ctx = md5(pack("!%dB"%len(hashdata),bodydata)).digest
|
||||
hashdata = unpack("!%dB"%len(dgst),dgst)
|
||||
print hashdata
|
||||
return hashdata
|
||||
|
||||
def _got_channel_auth_cap(self):
|
||||
pass
|
||||
|
||||
@ -134,15 +157,124 @@ class IPMISession:
|
||||
def login(self):
|
||||
self._initsession()
|
||||
self._get_channel_auth_cap()
|
||||
def _xmit_packet(self):
|
||||
if self.sockaddr:
|
||||
@classmethod
|
||||
def wait_for_rsp(cls,timeout=None):
|
||||
curtime=time()
|
||||
for session,parms in cls.waiting_sessions.iteritems():
|
||||
if timeout==0:
|
||||
break
|
||||
if parms['timeout'] <= curtime:
|
||||
timeout=0 #exit after one guaranteed pass
|
||||
if timeout is not None and timeout < parms['timeout']-curtime:
|
||||
continue #timeout is smaller than the current session would need
|
||||
timeout = parms['timeout']-curtime #set new timeout value
|
||||
if timeout is None:
|
||||
return len(cls.waiting_sessions)
|
||||
if cls.poller.poll(timeout*1000):
|
||||
while cls.poller.poll(0): #if the somewhat lengthy queue processing takes long enough for packets to come in, be eager
|
||||
pktqueue=deque([])
|
||||
while cls.poller.poll(0): #looks rendundant, but want to queue and process packets to keep things of RCVBUF
|
||||
rdata=cls.socket.recvfrom(3000)
|
||||
pktqueue.append(rdata)
|
||||
while len(pktqueue):
|
||||
(data,sockaddr)=pktqueue.popleft()
|
||||
cls._route_ipmiresponse(sockaddr,data)
|
||||
while cls.poller.poll(0): #seems ridiculous, but between every single callback, check for packets again
|
||||
rdata=cls.socket.recvfrom(3000)
|
||||
pktqueue.append(rdata)
|
||||
sessionstodel=[]
|
||||
for session,parms in cls.waiting_sessions.iteritems():
|
||||
if parms['timeout'] < curtime: #timeout has expired, time to give up on it and trigger timeout response in the respective session
|
||||
sessionstodel.append(session) #defer deletion until after loop as to avoid confusing the for loop
|
||||
cls.pending -= 1
|
||||
session._timedout()
|
||||
for session in sessionstodel:
|
||||
del cls.waiting_sessions[session]
|
||||
return len(cls.waiting_sessions)
|
||||
@classmethod
|
||||
def _route_ipmiresponse(cls,sockaddr,data):
|
||||
if not (data[0] == '\x06' and data[2:4] == '\xff\x07'): #packed data is not ipmi
|
||||
return
|
||||
for res in socket.getaddrinfo(self.bmc,self.port,0,socket.SOCK_DGRAM):
|
||||
sockaddr = res[4]
|
||||
if (res[0] == socket.AF_INET): #convert the sockaddr to AF_INET6
|
||||
newhost='::ffff:'+sockaddr[0]
|
||||
sockaddr = (newhost,sockaddr[1])
|
||||
IPMISession.socket.sendto(self.netpacket,sockaddr)
|
||||
try:
|
||||
cls.bmc_handlers[sockaddr]._handle_ipmi_packet(data)
|
||||
cls.pending-=1
|
||||
except KeyError:
|
||||
pass
|
||||
def _handle_ipmi_packet(self,data):
|
||||
if data[4] in ('\x00','\x02'): #This is an ipmi 1.5 paylod
|
||||
remsequencenumber = unpack('!I',data[5:9])[0]
|
||||
if hasattr(self,'remsequencenumber') and remsequencenumber < self.remsequencenumber:
|
||||
return -5 # remote sequence number is too low, reject it
|
||||
self.remsequencenumber=remsequencenumber
|
||||
if ord(data[4]) != self.authtype:
|
||||
return -2 #BMC responded with mismatch authtype, for the sake of mutual authentication reject it. If this causes legitimate issues, it's the vendor's fault
|
||||
remsessid = unpack("!I",data[9:13])[0]
|
||||
if remsessid != self.sessionid:
|
||||
return -1 #does not match our session id, drop it
|
||||
#new we need a mutable representation of the packet, rather than copying pieces of the packet over and over
|
||||
rsp=unpack("!%dB"%len(data),data)
|
||||
authcode=0
|
||||
if rsp[4] == 2: # we have an authcode in this ipmi 1.5 packet...
|
||||
authcode=rsp[13:29]
|
||||
del rsp[13:29]
|
||||
payload=list(rsp[14:14+rsp[13]])
|
||||
self._parse_ipmi_payload(payload)
|
||||
|
||||
|
||||
|
||||
def _parse_ipmi_payload(self,payload):
|
||||
#For now, skip the checksums since we are in LAN only, TODO: if implementing other channels, add checksum checks here
|
||||
if not (payload[4] == self.seqlun and payload[1]>>2 == self.expectednetfn and payload[5] == self.expectedcmd):
|
||||
return -1 #this payload is not a match for our outstanding ipmi packet
|
||||
if hasattr(self,'hasretried') and self.hasretried:
|
||||
self.hasretried=0
|
||||
self.tabooseq[(self.expectednetfn,self.expectedcmd,self.seqlun)]=16 # try to skip it for at most 16 cycles of overflow
|
||||
#We want to now remember that we do not have an expected packet
|
||||
self.expectednetfn=0x1ff #bigger than one byte means it can never match
|
||||
self.expectedcmd=0x1ff
|
||||
self.seqlun += 4 #prepare seqlun for next transmit
|
||||
self.seqlun &= 0xff #when overflowing, wrap around
|
||||
del IPMISession.waiting_sessions[self]
|
||||
self.lastpayload=None #render retry mechanism utterly incapable of doing anything, though it shouldn't matter
|
||||
self.last_payload_type=None
|
||||
del payload[0:5] # remove header of rsaddr/netfn/lun/checksum/rq/seq/lun
|
||||
del payload[-1] # remove the trailing checksum
|
||||
response={}
|
||||
response['cmd']=payload[0]
|
||||
response['code']=payload[1]
|
||||
del payload[0:2]
|
||||
response['data']=payload
|
||||
self.timeout=initialtimeout+(0.5*random())
|
||||
print repr(response)
|
||||
|
||||
def _timedout(self):
|
||||
#TODO: retransmit and error handling on lost packets
|
||||
pass
|
||||
|
||||
def _xmit_packet(self,waitforpending=True):
|
||||
if waitforpending:
|
||||
IPMISession.wait_for_rsp(timeout=0) #take a convenient opportunity to drain the socket queue if applicable
|
||||
while IPMISession.pending > IPMISession.maxpending:
|
||||
IPMISession.wait_for_rsp()
|
||||
IPMISession.waiting_sessions[self]={}
|
||||
IPMISession.waiting_sessions[self]['ipmisession']=self
|
||||
IPMISession.waiting_sessions[self]['timeout']=self.timeout+time()
|
||||
IPMISession.pending+=1
|
||||
if self.sockaddr:
|
||||
IPMISession.socket.sendto(self.netpacket,self.sockaddr)
|
||||
else: #he have not yet picked a working sockaddr for this connection, try all the candidates that getaddrinfo provides
|
||||
for res in socket.getaddrinfo(self.bmc,self.port,0,socket.SOCK_DGRAM):
|
||||
sockaddr = res[4]
|
||||
if (res[0] == socket.AF_INET): #convert the sockaddr to AF_INET6
|
||||
newhost='::ffff:'+sockaddr[0]
|
||||
sockaddr = (newhost,sockaddr[1],0,0)
|
||||
IPMISession.bmc_handlers[sockaddr]=self
|
||||
IPMISession.socket.sendto(self.netpacket,sockaddr)
|
||||
if self.sequencenumber: #seq number of zero will be left alone as it is special, otherwise increment
|
||||
self.sequencenumber += 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ipmis = IPMISession(bmc="10.240.181.1",userid="USERID",password="Passw0rd")
|
||||
while 1:
|
||||
IPMISession.wait_for_rsp()
|
||||
|
Loading…
x
Reference in New Issue
Block a user