2
0
mirror of https://opendev.org/x/pyghmi synced 2025-01-27 19:37:44 +00:00

Implement Serial-over-LAN (SOL) in BMC

This patch adds a BMC-side SOL-related class and methods:

  console.ServerConsole:
    Sub-class of console.Console for BMC

  bmc.Bmc.is_active():
    returns whether the BMC is active

  bmc.Bmc.activate_payload():
    activates SOL session

  bmc.Bmc.deactive_payload():
    deactivates SOL session

See bin/fakebmc and bin/virshbmc for examples.

Change-Id: I0e649ee0e2b45824a34c9634b00fd4f94c46c8de
This commit is contained in:
Akira Yoshiyama 2017-07-09 01:14:20 +09:00
parent d363fb3821
commit 756ebdbffb
5 changed files with 224 additions and 0 deletions

View File

@ -68,6 +68,15 @@ class FakeBmc(bmc.Bmc):
print 'politely shut down the system'
self.powerstate = 'off'
def is_active(self):
return self.powerstate == 'on'
def iohandler(self, data):
print(data)
if self.sol:
self.sol.send_data(data)
if __name__ == '__main__':
parser = argparse.ArgumentParser(
prog='fakebmc',

View File

@ -21,6 +21,30 @@ import argparse
import libvirt
import pyghmi.ipmi.bmc as bmc
import sys
import threading
def lifecycle_callback(connection, domain, event, detail, console):
console.state = console.domain.state(0)
def error_handler(unused, error):
if (error[0] == libvirt.VIR_ERR_RPC and
error[1] == libvirt.VIR_FROM_STREAMS):
return
def stream_callback(stream, events, self):
try:
data = self.stream.recv(1024)
except Exception:
return
if self.sol:
self.sol.send_data(data)
libvirt.virEventRegisterDefaultImpl()
libvirt.registerErrorHandler(error_handler, None)
class LibvirtBmc(bmc.Bmc):
@ -30,7 +54,12 @@ class LibvirtBmc(bmc.Bmc):
super(LibvirtBmc, self).__init__(authdata, port)
# Rely on libvirt to throw on bad data
self.conn = libvirt.open(hypervisor)
self.name = domain
self.domain = self.conn.lookupByName(domain)
self.state = self.domain.state(0)
self.stream = None
self.run_console = True
self.conn.domainEventRegister(lifecycle_callback, self)
def cold_reset(self):
# Reset of the BMC, not managed system, here we will exit the demo
@ -63,6 +92,32 @@ class LibvirtBmc(bmc.Bmc):
return 0xd5 # Not valid in this state
self.domain.shutdown()
def is_active(self):
return self.domain.isActive()
def activate_payload(self, request, session):
super(LibvirtBmc, self).activate_payload(request, session)
if self.stream is None:
self.stream = self.conn.newStream(libvirt.VIR_STREAM_NONBLOCK)
self.domain.openConsole(None, self.stream,
libvirt.VIR_DOMAIN_CONSOLE_FORCE)
self.stream.eventAddCallback(libvirt.VIR_STREAM_EVENT_READABLE,
stream_callback, self)
def deactivate_payload(self, request, session):
super(LibvirtBmc, self).deactivate_payload(request, session)
self.stream.eventRemoveCallback()
self.stream = None
def iohandler(self, data):
if self.stream:
self.stream.send(data)
def loop(self):
while True:
libvirt.virEventRunDefaultImpl()
if __name__ == '__main__':
parser = argparse.ArgumentParser(
prog='virshbmc',
@ -87,4 +142,6 @@ if __name__ == '__main__':
hypervisor=args.hypervisor,
domain=args.domain,
port=args.port)
t = threading.Thread(target=mybmc.loop)
t.start()
mybmc.listen()

View File

@ -15,6 +15,7 @@
# limitations under the License.
import pyghmi.ipmi.command as ipmicommand
import pyghmi.ipmi.console as console
import pyghmi.ipmi.private.serversession as serversession
import pyghmi.ipmi.private.session as ipmisession
import traceback
@ -23,6 +24,11 @@ __author__ = 'jjohnson2@lenovo.com'
class Bmc(serversession.IpmiServer):
activated = False
sol = None
iohandler = None
def cold_reset(self):
raise NotImplementedError
@ -47,6 +53,33 @@ class Bmc(serversession.IpmiServer):
def get_power_state(self):
raise NotImplementedError
def is_active(self):
raise NotImplementedError
def activate_payload(self, request, session):
if self.iohandler is None:
session.send_ipmi_response(code=0x81)
elif not self.is_active():
session.send_ipmi_response(code=0x81)
elif self.activated:
session.send_ipmi_response(code=0x80)
else:
self.activated = True
session.send_ipmi_response(
data=[0, 0, 0, 0, 1, 0, 1, 0, 2, 0x6f, 0xff, 0xff])
self.sol = console.ServerConsole(session, self.iohandler)
def deactivate_payload(self, request, session):
if self.iohandler is None:
session.send_ipmi_response(code=0x81)
elif not self.activated:
session.send_ipmi_response(code=0x80)
else:
session.send_ipmi_response()
self.sol.close()
self.activated = False
self.sol = None
@staticmethod
def handle_missing_command(session):
session.send_ipmi_response(code=0xc1)
@ -131,6 +164,10 @@ class Bmc(serversession.IpmiServer):
return self.send_device_id(session)
elif request['command'] == 2: # cold reset
return session.send_ipmi_response(code=self.cold_reset())
elif request['command'] == 0x48: # activate payload
return self.activate_payload(request, session)
elif request['command'] == 0x49: # deactivate payload
return self.deactivate_payload(request, session)
elif request['netfn'] == 0:
if request['command'] == 1: # get chassis status
return self.get_chassis_status(session)

View File

@ -386,3 +386,123 @@ class Console(object):
# own session
while (1):
session.Session.wait_for_rsp(timeout=600)
class ServerConsole(Console):
"""IPMI SOL class.
This object represents an SOL channel, multiplexing SOL data with
commands issued by ipmi.command.
:param session: IPMI session
:param iohandler: I/O handler
"""
def __init__(self, _session, iohandler, force=False):
self.keepaliveid = None
self.connected = True
self.broken = False
self.out_handler = iohandler
self.remseq = 0
self.myseq = 0
self.lastsize = 0
self.retriedpayload = 0
self.pendingoutput = []
self.awaitingack = False
self.activated = True
self.force_session = force
self.ipmi_session = _session
self.ipmi_session.sol_handler = self._got_sol_payload
self.maxoutcount = 256
self.poweredon = True
session.Session.wait_for_rsp(0)
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
if type(payload) == dict: # we received an error condition
self.activated = False
self._print_error(payload)
return
newseq = payload[0] & 0b1111
ackseq = payload[1] & 0b1111
ackcount = payload[2]
nacked = payload[3] & 0b1000000
breakdetected = 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
flag = 0
if not self.poweredon:
flag |= 0b1100000
if not self.activated:
flag |= 0b1010000
if newseq != 0: # this packet at least has some data to send to us..
if len(payload) > 4:
remdatalen = len(payload[4:]) # store remote 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
ackpayload = (0, self.remseq, remdatalen, flag)
# 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
try:
self.send_payload(ackpayload, retry=False)
except exc.IpmiException:
# if the session is broken, then close the SOL session
self.close()
if remdata: # Do not subject callers to empty data
self._print_data(remdata)
if self.myseq != 0 and ackseq == self.myseq: # the bmc has something
# to say about last xmit
self.awaitingack = False
if nacked and not breakdetected: # the BMC was in some way unhappy
newtext = self.lastpayload[4 + ackcount:]
newtext = struct.pack("B"*len(newtext), *newtext)
if (self.pendingoutput and
not isinstance(self.pendingoutput[0], dict)):
self.pendingoutput[0] = newtext + self.pendingoutput[0]
else:
self.pendingoutput = [newtext] + self.pendingoutput
self._sendpendingoutput()
if len(self.pendingoutput) > 0:
self._sendpendingoutput()
elif ackseq != 0 and self.awaitingack:
# if an ack packet came in, but did not match what we
# expected, retry our payload now.
# the situation that was triggered was a senseless retry
# when data came in while we xmitted. In theory, a BMC
# should handle a retry correctly, but some do not, so
# try to mitigate by avoiding overeager retries
# occasional retry of a packet
# sooner than timeout suggests is evidently a big deal
self.send_payload(payload=self.lastpayload)
def send_payload(self, payload, payload_type=1, retry=True,
needskeepalive=False):
while not (self.connected or self.broken):
session.Session.wait_for_rsp(timeout=10)
self.ipmi_session.send_payload(payload,
payload_type=payload_type,
retry=retry,
needskeepalive=needskeepalive)
def close(self):
"""Shut down an SOL session,
"""
self.activated = False

View File

@ -76,6 +76,7 @@ class ServerSession(ipmisession.Session):
self.kg = kg
self.socket = netsocket
self.sockaddr = clientaddr
self.pendingpayloads = collections.deque([])
self.pktqueue = collections.deque([])
ipmisession.Session.bmc_handlers[clientaddr] = self
response = self.create_open_session_response(bytearray(request))