2
0
mirror of https://opendev.org/x/pyghmi synced 2025-07-13 16:01:32 +00:00

Add SOL support

SOL support is added in a manner that is actually functional

Change-Id: I3f83e06b27a0d44038ac6e6afcd4f8af1c534946
This commit is contained in:
Jarrod Johnson
2013-07-02 17:03:24 -04:00
parent a7b4b4a71d
commit 02e353f2fb
3 changed files with 490 additions and 75 deletions

252
ipmi/console.py Normal file
View File

@ -0,0 +1,252 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 IBM Corporation
#
# 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 low layer message framing portion of IPMI
import fcntl
import os
import struct
from ipmi.private import session
from ipmi.private import constants
class Console(object):
"""IPMI SOL class.
This object represents an SOL channel, multiplexing SOL data with
commands issued by ipmi.command.
:param bmc: hostname or ip address of BMC
:param userid: username to use to connect
:param password: password to connect to the BMC
:param iohandler: Either a function to call with bytes, a filehandle to
use for input and output, or a tuple of (input, output)
handles
:param kg: optional parameter for BMCs configured to require it
"""
#TODO(jbjohnso): still need an exit and a data callin function
def __init__(self, bmc, userid, password,
iohandler=None,
force=False, kg=None):
if type(iohandler) == tuple: #two file handles
self.console_in = iohandler[0]
self.console_out = iohandler[1]
elif type(iohandler) == file: # one full duplex file handle
self.console_out = iohandler
self.console_in = iohandler
elif type(iohander) == types.FunctionType:
self.console_out = None
self.out_handler = iohandler
else:
raise(Exception('No IO handler provided'))
if self.console_in is not None:
fcntl.fcntl(self.console_in.fileno(), fcntl.F_SETFL, os.O_NONBLOCK)
self.remseq = 0
self.myseq = 0
self.lastsize = 0
self.sendbreak = 0
self.ackedcount = 0
self.ackedseq = 0
self.retriedpayload = 0
self.pendingoutput=""
self.awaitingack=False
self.force_session = force
self.ipmi_session = session.Session(bmc=bmc,
userid=userid,
password=password,
kg=kg,
onlogon=self._got_session
)
def _got_session(self, response):
"""Private function to navigate SOL payload activation
"""
if 'error' in response:
self._print_data(response['error'])
return
#Send activate sol payload directive
#netfn= 6 (application)
#command = 0x48 (activate payload)
#data = (1, sol payload type
# 1, first instance
# 0b11000000, -encrypt, authenticate,
# disable serial/modem alerts, CTS fine
# 0, 0, 0 reserved
self.ipmi_session.raw_command(netfn=0x6, command=0x48,
data=(1, 1, 192, 0, 0, 0),
callback=self._payload_activated)
def _payload_activated(self, response):
"""Check status of activate payload request
"""
if 'error' in response:
self._print_data(response['error'])
#given that these are specific to the command,
#it's probably best if one can grep the error
#here instead of in constants
sol_activate_codes = {
0x81: 'SOL is disabled',
0x82: 'Maximum SOL session count reached',
0x83: 'Cannot activate payload with encryption',
0x84: 'Cannot activate payload without encryption',
}
if response['code']:
if response['code'] in constants.ipmi_completion_codes:
self._print_data(
constants.ipmi_completion_codes[response['code']])
return
elif response['code'] == 0x80:
if self.force_session and not self.retriedpayload:
self.retriedpayload=1
self.ipmi_session.raw_command(netfn=0x6, command=0x49,
data=(1, 1, 0, 0, 0, 0),
callback=self._got_session)
return
else:
self._print_data('SOL Session active for another client\n')
return
elif response['code'] in sol_activate_codes:
self._print_data(sol_activate_codes[response['code']]+'\n')
return
else:
self._print_data(
'SOL encountered Unrecognized error code %d\n' %
response['code'])
return
#data[0:3] is reserved except for the test mode, which we don't use
data = response['data']
self.maxoutcount = (data[5] << 8) + data[4]
#BMC tells us this is the maximum allowed size
#data[6:7] is the promise of how small packets are going to be, but we
#don't have any reason to worry about it
if (data[8] + (data[9] << 8)) != 623:
raise Exception("TODO(jbjohnso): support atypical SOL port number")
#ignore data[10:11] for now, the vlan detail, shouldn't matter to this
#code anyway...
self.ipmi_session.sol_handler=self._got_sol_payload
if self.console_in is not None:
self.ipmi_session.register_handle_callback(self.console_in,
self._got_cons_input)
def _got_cons_input(self,handle):
"""Callback for handle events detected by ipmi session
"""
self.pendingoutput += handle.read()
if not self.awaitingack:
self._sendpendingoutput()
def _sendpendingoutput(self):
self.myseq += 1
self.myseq &= 0xf
if self.myseq == 0:
self.myseq = 1
payload = struct.pack("BBBB",
self.myseq,
self.ackedseq,
self.ackedseq,
self.sendbreak)
payload += self.pendingoutput
self.lasttextsize = len(self.pendingoutput)
self.pendingoutput = ""
self.awaitingack = True
payload = struct.unpack("%dB" % len(payload), payload)
self.lastpayload=payload
self.ipmi_session.send_payload(payload, payload_type=1)
def _print_data(self,data):
"""Convey received data back to caller in the format of their choice.
Caller may elect to provide this class filehandle(s) or else give a
callback function that this class will use to convey data back to
caller.
"""
if self.console_out is not None:
self.console_out.write(data)
self.console_out.flush()
elif self.out_handler: #callback style..
self.out_handler(data)
def _got_sol_payload(self, payload):
"""SOL payload callback
"""
#TODO(jbjohnso) test cases to throw some likely scenarios at functions
#for example, retry with new data, retry with no new data
#retry with unexpected sequence number
newseq = payload[0] & 0b1111
ackseq = payload[1] & 0b1111
ackcount = payload[2]
nacked = payload[3] & 0b1000000
poweredoff = payload[3] & 0b100000
deactivated = payload[3] & 0b10000
#for now, ignore overrun. I assume partial NACK for this reason or for
#no reason would be treated the same, new payload with partial data
remdata = ""
remdatalen = 0
if newseq != 0: #this packet at least has some data to send to us..
if len(payload) > 4:
remdatalen = len(payload[4:]) #store remote data len before dupe
#retry logic, we must ack *this* many even if it is
#a retry packet with new partial data
remdata = struct.pack("%dB" % remdatalen, *payload[4:])
if newseq == self.remseq: #it is a retry, but could have new data..
if remdatalen > self.lastsize:
remdata = remdata[4 + self.lastsize:]
else: # no new data...
remdata = ""
else: #TODO(jbjohnso) what if remote sequence number is wrong??
self.remseq = newseq
self.lastsize=remdatalen
self._print_data(remdata)
ackpayload = (0, self.remseq, remdatalen, 0)
#Why not put pending data into the ack? because it's rare
#and might be hard to decide what to do in the context of
#retry situation
self.ipmi_session.send_payload(ackpayload,
payload_type=1, retry=False)
if self.myseq != 0 and ackseq == self.myseq: #the bmc has something to
#say about our last xmit
self.awaitingack=False
if nacked > 0: #the BMC was in some way unhappy
if poweredoff:
self._print_data("Remote system is powered down\n")
if deactivated:
self._print_data("Remote IPMI console disconnected\n")
else: #retry all or part of packet, but in a new form
#also add pending output for efficiency and ease
newtext = self.lastpayload[4 + ackcount:]
self.pendingoutput = newtext + self.pendingoutput
self._sendpendingoutput()
elif self.awaitingack: #session has marked us as happy, but we are not
#this does mean that we will occasionally retry a packet
#sooner than retry suggests, but that's no big deal
self.ipmi_session.send_payload(payload=self.lastpayload,
payload_type=1)
def main_loop(self):
"""Process all events until no more sessions exist.
If a caller is a simple little utility, provide a function to
eternally run the event loop. More complicated usage would be expected
to provide their own event loop behavior, though this could be used
within the greenthread implementation of caller's choice if desired.
"""
#wait_for_rsp promises to return a false value when no sessions are
#alive anymore
#TODO(jbjohnso): wait_for_rsp is not returning a true value from our own
#session
while (1):
session.Session.wait_for_rsp(timeout=600)

View File

@ -92,7 +92,29 @@ def get_ipmi_error(response, suffix=""):
class Session:
"""A class to manage common IPMI session logistics
Almost all developers should not worry about this class and instead be
looking toward ipmi.Command and ipmi.Console.
For those that do have to worry, the main interesting thing is that the
event loop can go one of two ways. Either a larger manager can query using
class methods
the soonest timeout deadline and the filehandles to poll and assume
responsibility for the polling, or it can register filehandles to be
watched. This is primarily of interest to Console class, which may have an
input filehandle to watch and can pass it to Session.
:param bmc: hostname or ip address of the BMC
:param userid: username to use to connect
:param password: password to connect to the BMC
:param kg: optional parameter if BMC requires Kg be set
:param port: UDP port to communicate with, pretty much always 623
:param onlogon: callback to receive notification of login completion
"""
poller = select.poll()
ipmipoller = select.poll()
_external_handlers = {}
bmc_handlers = {}
waiting_sessions = {}
peeraddr_to_nodes = {}
@ -126,6 +148,7 @@ class Session:
curmax = cls.socket.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF)
cls.poller.register(cls.socket, select.POLLIN)
cls.ipmipoller.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
@ -156,11 +179,12 @@ class Session:
kg=None,
onlogon=None,
onlogonargs=None):
self.lastpayload = None
self.bmc = bmc
self.userid = userid
self.password = password
self.noretry = False
self.nowait = False
self.pendingpayloads = collections.deque([])
if kg is not None:
self.kg = kg
else:
@ -222,6 +246,8 @@ class Session:
# this should gracefully be backwards compat, but some
# 1.5 implementations checked reserved bits
self.ipmi15only = 0
self.sol_handler = None
# NOTE(jbjohnso): This is the callback handler for any SOL payload
def _checksum(self, *data): # Two's complement over the data
csum = sum(data)
@ -268,6 +294,7 @@ class Session:
netfn,
command,
data=[],
retry=True,
callback=None,
callback_args=None):
self.ipmicallbackargs = callback_args
@ -276,37 +303,57 @@ class Session:
self.ipmicallback = self._generic_callback
else:
self.ipmicallback = callback
self._send_ipmi_net_payload(netfn, command, data)
self._send_ipmi_net_payload(netfn, command, data, retry=retry)
if retry: #in retry case, let the retry timers auto-indicate wait time
timeout=None
else: #if not retry, give it a second before surrending
timeout=1
#In the synchronous case, wrap the event loop in this call
#The event loop is shared amongst python-ipmi session instances
#within a process. In this way, synchronous usage of the interface
#plays well with asynchronous use. In fact, this produces the behavior
#of only the constructor *really* needing a callback. From then on,
#synchronous usage of the class acts in a greenthread manner governed by
#order of data on the network
if callback is None:
while self.lastresponse is None:
Session.wait_for_rsp()
Session.wait_for_rsp(timeout=timeout)
return self.lastresponse
def _send_ipmi_net_payload(self, netfn, command, data):
def _send_ipmi_net_payload(self, netfn, command, data, retry=True):
ipmipayload = self._make_ipmi_payload(netfn, command, data)
payload_type = constants.payload_types['ipmi']
self.send_payload(payload=ipmipayload, payload_type=payload_type,
retry=retry)
def send_payload(self, payload=None, payload_type=None, retry=True):
if self.lastpayload is not None:
#we already have a packet outgoing, make this
# a pending payload
# this way a simplistic BMC won't get confused
# and we also avoid having to do a more complicated
# retry mechanism where each payload is
# retried separately
self.pendingpayloads.append((payload,payload_type,retry))
return
if payload_type is None:
payload_type = self.last_payload_type
if payload is None:
payload = self.lastpayload
message = [0x6, 0, 0xff, 0x07] # constant RMCP header for IPMI
if retry:
self.lastpayload = payload
self.last_payload_type = payload_type
message.append(self.authtype)
baretype = payload_type
if self.integrityalgo:
payload_type |= 0b01000000
if self.confalgo:
payload_type |= 0b10000000
self.send_payload(payload=ipmipayload, payload_type=payload_type)
def send_payload(self, payload=None, payload_type=None):
if payload is None:
payload = self.lastpayload
if payload_type is None:
payload_type = self.last_payload_type
message = [0x6, 0, 0xff, 0x07] # constant RMCP header for IPMI
baretype = payload_type & 0b00111111
self.lastpayload = payload
self.last_payload_type = payload_type
message.append(self.authtype)
if (self.ipmiversion == 2.0):
message.append(payload_type)
if (baretype == 2):
raise Exception("TODO(jbjohnso): OEM Payloads")
elif (baretype == 1):
raise Exception("TODO(jbjohnso): SOL Payload")
elif baretype not in constants.payload_types.values():
raise Exception("Unrecognized payload type %d" % baretype)
message += struct.unpack("!4B", struct.pack("<I", self.sessionid))
@ -326,7 +373,7 @@ class Session:
psize = len(payload)
if self.confalgo:
pad = (
psize + 1) % 16 # pad has to account for one byte field as in
psize + 1) % 16 # pad has to cope with one byte field like
# the _aespad function
if pad: # if no pad needed, then we take no more action
pad = 16 - pad
@ -340,7 +387,8 @@ class Session:
message += list(struct.unpack("16B", iv))
payloadtocrypt = _aespad(payload)
crypter = AES.new(self.aeskey, AES.MODE_CBC, iv)
crypted = crypter.encrypt(struct.pack("%dB" % len(payloadtocrypt),
crypted = crypter.encrypt(struct.pack("%dB" %
len(payloadtocrypt),
*payloadtocrypt))
crypted = list(struct.unpack("%dB" % len(crypted), crypted))
message += crypted
@ -367,7 +415,7 @@ class Session:
# per RFC2404 truncates to 96 bits
message += struct.unpack("12B", authcode)
self.netpacket = struct.pack("!%dB" % len(message), *message)
self._xmit_packet()
self._xmit_packet(retry)
def _ipmi15authcode(self, payload, checkremotecode=False):
if self.authtype == 0: # Only for things prior to auth in ipmi 1.5, not
@ -380,12 +428,15 @@ class Session:
password += '\x00' * padneeded
passdata = struct.unpack("16B", password)
if checkremotecode:
seqbytes = struct.unpack("!4B", struct.pack("<I", self.remsequencenumber))
seqbytes = struct.unpack("!4B",
struct.pack("<I", self.remsequencenumber))
else:
seqbytes = struct.unpack("!4B", struct.pack("<I", self.sequencenumber))
seqbytes = struct.unpack("!4B",
struct.pack("<I", self.sequencenumber))
sessdata = struct.unpack("!4B", struct.pack("<I", self.sessionid))
bodydata = passdata + sessdata + tuple(payload) + seqbytes + passdata
dgst = hashlib.md5(struct.pack("%dB" % len(bodydata), *bodydata)).digest()
dgst = hashlib.md5(
struct.pack("%dB" % len(bodydata), *bodydata)).digest()
hashdata = struct.unpack("!%dB" % len(dgst), dgst)
return hashdata
@ -412,9 +463,9 @@ class Session:
if self.ipmiversion == 1.5:
if not (data[1] & 0b100):
call_with_optional_args(self.onlogon,
{'error':
"MD5 is required but not enabled/available on target BMC"},
self.onlogonargs)
{'error':
"MD5 is required but not enabled/available on target BMC"},
self.onlogonargs)
return
self._get_session_challenge()
elif self.ipmiversion == 2.0:
@ -486,8 +537,8 @@ class Session:
def _open_rmcpplus_request(self):
self.authtype = 6
self.localsid += 1 # have unique local session ids to ignore aborted login
# attempts from the past
self.localsid += 1 # have unique local session ids to ignore aborted
# login attempts from the past
self.rmcptag += 1
data = [
self.rmcptag,
@ -499,11 +550,11 @@ class Session:
0, 0, 0, 8, 1, 0, 0, 0, # table 13-17, SHA-1
1, 0, 0, 8, 1, 0, 0, 0, # SHA-1 integrity
2, 0, 0, 8, 1, 0, 0, 0, # AES privacy
# 2,0,0,8,0,0,0,0, #no privacy confalgo
#2,0,0,8,0,0,0,0, #no privacy confalgo
]
self.sessioncontext = 'OPENSESSION'
self.send_payload(payload=data,
payload_type=constants.payload_types['rmcpplusopenreq'])
payload_type=constants.payload_types['rmcpplusopenreq'])
def _get_channel_auth_cap(self):
self.ipmicallback = self._got_channel_auth_cap
@ -523,6 +574,22 @@ class Session:
@classmethod
def wait_for_rsp(cls, timeout=None):
"""IPMI Session Event loop iteration
This watches for any activity on IPMI handles and handles registered
by register_handle_callback. Callers are satisfied in the order that
packets return from nework, not in the order of calling.
:param timeout: Maximum time to wait for data to come across. If
unspecified, will autodetect based on earliest timeout
"""
#Assume:
#Instance A sends request to packet B
#Then Instance C sends request to BMC D
#BMC D was faster, so data comes back before BMC B
#Instance C gets to go ahead of Instance A, because
#Instance C can get work done, but instance A cannot
curtime = time.time()
for session, parms in cls.waiting_sessions.iteritems():
if timeout == 0:
@ -531,42 +598,66 @@ class Session:
timeout = 0 # exit after one guaranteed pass
continue
if timeout is not None and timeout < parms['timeout'] - curtime:
continue # timeout is smaller than the current session would need
continue # timeout is smaller than the current session needs
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
while cls.ipmipoller.poll(0): # if the somewhat lengthy queue
# processing takes long enough for packets to come in,
# be eager
pktqueue = collections.deque([])
while cls.poller.poll(0): # looks rendundant, but want to queue
# and process packets to keep things off RCVBUF
while cls.ipmipoller.poll(0): # looks rendundant, but want to
#queue and process packets to keep things off
#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
while cls.ipmipoller.poll(0): # seems ridiculous, but
# between every single callback, check for packets again
rdata = cls.socket.recvfrom(3000)
pktqueue.append(rdata)
for handlepair in cls.poller.poll(0):
myhandle = handlepair[0]
if myhandle != cls.socket.fileno():
myfile = cls._external_handlers[myhandle][1]
cls._external_handlers[myhandle][0](myfile)
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
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
# to avoid confusing the for loop
for session in sessionstodel:
cls.pending -= 1
del cls.waiting_sessions[session]
session.lastpayload = None
cls.waiting_sessions.pop(session,None)
session._timedout()
return len(cls.waiting_sessions)
@classmethod
def register_handle_callback(cls, handle, callback):
"""Add a handle to be watched by Session's event loop
In the event that an application would like IPMI Session event loop
to drive things while adding their own filehandle to watch for events,
this class method will register that.
:param handle: filehandle too watch for input
:param callback: function to call when input detected on the handle.
will receive the handle as an argument
"""
cls._external_handlers[handle.fileno()]=(callback,handle)
cls.poller.register(handle, select.POLLIN)
@classmethod
def _route_ipmiresponse(cls, sockaddr, data):
if not (data[0] == '\x06' and data[2:4] == '\xff\x07'): # not valid ipmi
if not (data[0] == '\x06' and data[2:4] == '\xff\x07'): # not ipmi
return
try:
cls.bmc_handlers[sockaddr]._handle_ipmi_packet(data,
@ -591,7 +682,7 @@ class Session:
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
return -2 # BMC responded with mismatch authtype, for
# mutual authentication reject it. If this causes
# legitimate issues, it's the vendor's fault
remsessid = struct.unpack("<I", data[9:13])[0]
@ -601,7 +692,7 @@ class Session:
# copying pieces of the packet over and over
rsp = list(struct.unpack("!%dB" % len(data), data))
authcode = False
if data[4] == '\x02': # we have an authcode in this ipmi 1.5 packet...
if data[4] == '\x02': # we have an authcode in this ipmi 1.5 packet
authcode = data[13:29]
del rsp[13:29]
# this is why we needed a mutable representation
@ -631,7 +722,7 @@ class Session:
return self._got_rakp2(data[16:])
elif ptype == 0x15:
return self._got_rakp4(data[16:])
elif ptype == 0: # good old ipmi payload
elif ptype == 0 or ptype == 1: # good old ipmi payload or sol
# If I'm endorsing a shared secret scheme, then at the very least it
# needs to do mutual assurance
if not (data[5] & 0b01000000): # This would be the line that might
@ -660,12 +751,25 @@ class Session:
if encrypted:
iv = rawdata[16:32]
decrypter = AES.new(self.aeskey, AES.MODE_CBC, iv)
decrypted = decrypter.decrypt(struct.pack("%dB" % len(payload[16:]),
*payload[16:]))
decrypted = decrypter.decrypt(
struct.pack("%dB" % len(payload[16:]),
*payload[16:]))
payload = struct.unpack("%dB" % len(decrypted), decrypted)
padsize = payload[-1] + 1
payload = list(payload[:-padsize])
self._parse_ipmi_payload(payload)
if ptype == 0:
self._parse_ipmi_payload(payload)
elif ptype == 1: #There should be no other option
#note that we assume the SOL payload is good enough to avoid
# retry SOL logic is sufficiently different, we just
# defer that call to the sol handler, it can re submit if it
# is unhappy
if self.last_payload_type == 1: #but only if SOL was last sent
self.lastpayload = None
self.last_payload_type = None
Session.waiting_sessions.pop(self,None)
if self.sol_handler:
self.sol_handler(payload)
def _got_rmcp_response(self, data):
# see RMCP+ open session response table
@ -688,9 +792,11 @@ class Session:
localsid = struct.unpack("<I", struct.pack("4B", *data[4:8]))[0]
if self.localsid != localsid:
return -9
self.pendingsessionid = struct.unpack("<I", struct.pack("4B", *data[8:12]))[0]
self.pendingsessionid = struct.unpack("<I",
struct.pack("4B", *data[8:12]))[0]
# TODO(jbjohnso): currently, we take it for granted that the responder
# accepted our integrity/auth/confidentiality proposal
self.lastpayload = None
self._send_rakp1()
def _send_rakp1(self):
@ -698,11 +804,11 @@ class Session:
self.randombytes = os.urandom(16)
userlen = len(self.userid)
payload = [self.rmcptag, 0, 0, 0] + \
list(struct.unpack("4B", struct.pack("<I", self.pendingsessionid))) +\
list(struct.unpack("16B", self.randombytes)) +\
[self.privlevel, 0, 0] +\
[userlen] +\
list(struct.unpack("%dB" % userlen, self.userid))
list(struct.unpack("4B", struct.pack("<I", self.pendingsessionid))) +\
list(struct.unpack("16B", self.randombytes)) +\
[self.privlevel, 0, 0] +\
[userlen] +\
list(struct.unpack("%dB" % userlen, self.userid))
self.sessioncontext = "EXPECTINGRAKP2"
self.send_payload(
payload=payload, payload_type=constants.payload_types['rakp1'])
@ -754,6 +860,7 @@ class Session:
self.k2 = HMAC.new(self.sik, '\x02' * 20, SHA).digest()
self.aeskey = self.k2[0:16]
self.sessioncontext = "EXPECTINGRAKP4"
self.lastpayload = None
self._send_rakp3()
def _send_rakp3(self): # rakp message 3
@ -814,6 +921,7 @@ class Session:
self.confalgo = 'aes'
self.sequencenumber = 1
self.sessioncontext = 'ESTABLISHED'
self.lastpayload = None
self._req_priv_level()
'''
@ -824,8 +932,9 @@ class Session:
# For now, skip the checksums since we are in LAN only,
# TODO(jbjohnso): if implementing other channels, add checksum checks
# here
if (payload[4] != self.seqlun or payload[1] >> 2 != self.expectednetfn or
payload[5] != self.expectedcmd):
if (payload[4] != self.seqlun or
payload[1] >> 2 != self.expectednetfn or
payload[5] != self.expectedcmd):
return -1 # this payload is not a match for our outstanding packet
if hasattr(self, 'hasretried') and self.hasretried:
self.hasretried = 0
@ -833,13 +942,14 @@ class Session:
(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.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
del Session.waiting_sessions[self]
self.lastpayload = None # render retry mechanism utterly incapable of doing
# anything, though it shouldn't matter
Session.waiting_sessions.pop(self,None)
self.lastpayload = None # render retry mechanism utterly incapable of
#doing anything, though it shouldn't matter
self.last_payload_type = None
response = {}
response['netfn'] = payload[1] >> 2
@ -851,6 +961,12 @@ class Session:
del payload[0:2]
response['data'] = payload
self.timeout = initialtimeout + (0.5 * random.random())
if len(self.pendingpayloads) > 0:
(nextpayload, nextpayloadtype, retry) = \
self.pendingpayloads.popleft()
self.send_payload(payload=nextpayload,
payload_type=nextpayloadtype,
retry=nextretry)
call_with_optional_args(self.ipmicallback,
response,
self.ipmicallbackargs)
@ -860,8 +976,6 @@ class Session:
return
self.nowait = True
self.timeout += 1
if self.noretry:
return
if self.timeout > 5:
response = {'error': 'timeout'}
call_with_optional_args(self.ipmicallback,
@ -879,7 +993,7 @@ class Session:
self._open_rmcpplus_request()
elif (self.sessioncontext == 'EXPECTINGRAKP2' or
self.sessioncontext == 'EXPECTINGRAKP4'):
# If we can't be sure which RAKP was dropped or that RAKP3/4 was just
# If we can't be sure which RAKP was dropped or if RAKP3/4 was just
# delayed, the most reliable thing to do is rewind and start over
# bmcs do not take kindly to receiving RAKP1 or RAKP3 twice
self._relog()
@ -889,13 +1003,14 @@ class Session:
# and chassis reset, which sometimes will shoot itself
# systematically in the head in a shared port case making replies
# impossible
self.hasretried = 1 # remember so that we can track taboo combinations
self.hasretried = 1 # remember so that we can track taboo
# combinations
# of sequence number, netfn, and lun due to
# ambiguity on the wire
self.send_payload()
self.nowait = False
def _xmit_packet(self):
def _xmit_packet(self, retry=True):
if not self.nowait: # if we are retrying, we really need to get the
# packet out and get our timeout updated
Session.wait_for_rsp(timeout=0) # take a convenient opportunity
@ -903,10 +1018,12 @@ class Session:
# applicable
while Session.pending > Session.maxpending:
Session.wait_for_rsp()
Session.waiting_sessions[self] = {}
Session.waiting_sessions[self]['ipmisession'] = self
Session.waiting_sessions[self]['timeout'] = self.timeout + time.time()
Session.pending += 1
if retry:
Session.waiting_sessions[self] = {}
Session.waiting_sessions[self]['ipmisession'] = self
Session.waiting_sessions[self]['timeout'] = self.timeout + \
time.time()
Session.pending += 1
if self.sockaddr:
Session.socket.sendto(self.netpacket, self.sockaddr)
else: # he have not yet picked a working sockaddr for this connection,
@ -916,12 +1033,12 @@ class Session:
0,
socket.SOCK_DGRAM):
sockaddr = res[4]
if (res[0] == socket.AF_INET): # convert the sockaddr to AF_INET6
if (res[0] == socket.AF_INET): #convert the sockaddr AF_INET6
newhost = '::ffff:' + sockaddr[0]
sockaddr = (newhost, sockaddr[1], 0, 0)
Session.bmc_handlers[sockaddr] = self
Session.socket.sendto(self.netpacket, sockaddr)
if self.sequencenumber: # seq number of zero will be left alone as it is
if self.sequencenumber: # seq number of zero will be left alone, it is
# special, otherwise increment
self.sequencenumber += 1
@ -931,11 +1048,11 @@ class Session:
return {'success': True}
callback({'success': True})
return
self.noretry = True # risk open sessions if logout request gets dropped,
# logout is not idempotent so this is the better bet
self.raw_command(command=0x3c,
netfn=6,
data=struct.unpack("4B", struct.pack("I", self.sessionid)),
data=struct.unpack("4B",
struct.pack("I", self.sessionid)),
retry=False,
callback=callback,
callback_args=callback_args)
self.logged = 0

46
solconnect.py Normal file
View File

@ -0,0 +1,46 @@
#!/usr/bin/env python
"""
@author: Jarrod Johnson <jbjohnso@us.ibm.com>
Copyright 2013 IBM Corporation
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.
"""
"""A simple little script to exemplify/test ipmi.console module
"""
import os
import sys
import fcntl
import tty
import termios
from ipmi import console
tcattr = termios.tcgetattr(sys.stdin)
newtcattr = tcattr
#TODO: allow ctrl-c and crtl-z to go to remote console, add our own exit handler
newtcattr[-1][termios.VINTR] = 0
newtcattr[-1][termios.VSUSP] = 0
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, newtcattr)
tty.setcbreak(sys.stdin.fileno())
passwd = os.environ['IPMIPASSWORD']
try:
sol = console.Console(bmc=sys.argv[1], userid=sys.argv[2], password=passwd,
iohandler=(sys.stdin, sys.stdout), force=True)
sol.main_loop()
finally:
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, tcattr)