diff --git a/ipmibase.py b/ipmibase.py index a06ad665..fecf915c 100644 --- a/ipmibase.py +++ b/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()