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:
252
ipmi/console.py
Normal file
252
ipmi/console.py
Normal 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)
|
@ -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
46
solconnect.py
Normal 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)
|
Reference in New Issue
Block a user