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:
parent
d363fb3821
commit
756ebdbffb
@ -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',
|
||||
|
57
bin/virshbmc
57
bin/virshbmc
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
Loading…
x
Reference in New Issue
Block a user