Skip to content

Instantly share code, notes, and snippets.

@yosshy
Last active July 10, 2017 07:41
Show Gist options
  • Save yosshy/d0b96e387d5dafec9def05f20a492181 to your computer and use it in GitHub Desktop.
Save yosshy/d0b96e387d5dafec9def05f20a492181 to your computer and use it in GitHub Desktop.
SOL capability for IPMI server in pyghmi
commit 529fa89de14a4e2027dbfad79d7412b57b91f88a
Author: Akira Yoshiyama <[email protected]>
Date: Sun Jul 9 01:14:20 2017 +0900
Added SOL capability to BMC
diff --git a/bin/fakebmc b/bin/fakebmc
index 318970e..2647f11 100755
--- a/bin/fakebmc
+++ b/bin/fakebmc
@@ -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',
diff --git a/bin/virshbmc b/bin/virshbmc
index 5dabe08..36390e0 100755
--- a/bin/virshbmc
+++ b/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:
+ 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,31 @@ 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 +141,6 @@ if __name__ == '__main__':
hypervisor=args.hypervisor,
domain=args.domain,
port=args.port)
+ t = threading.Thread(target=mybmc.loop)
+ t.start()
mybmc.listen()
diff --git a/pyghmi/ipmi/bmc.py b/pyghmi/ipmi/bmc.py
index 4d6c23b..dd3d68a 100644
--- a/pyghmi/ipmi/bmc.py
+++ b/pyghmi/ipmi/bmc.py
@@ -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__ = '[email protected]'
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)
diff --git a/pyghmi/ipmi/console.py b/pyghmi/ipmi/console.py
index 90ce2c8..ad41544 100644
--- a/pyghmi/ipmi/console.py
+++ b/pyghmi/ipmi/console.py
@@ -386,3 +386,135 @@ 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
+ ring = payload[3] & 0b100000
+ breakdetected = payload[3] & 0b10000
+ cts = payload[3] & 0b1000
+ dcd_dsr = payload[3] & 0b100
+ flush_inbound = payload[3] & 0b10
+ flush_outbound = payload[3] & 0b1
+ # 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
+ if poweredoff:
+ self._print_info("Remote system is powered down")
+ if deactivated:
+ self.activated = False
+ self._print_error("Remote IPMI console disconnected")
+ 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:]
+ 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
diff --git a/pyghmi/ipmi/private/serversession.py b/pyghmi/ipmi/private/serversession.py
index dbbe143..ee8cea6 100644
--- a/pyghmi/ipmi/private/serversession.py
+++ b/pyghmi/ipmi/private/serversession.py
@@ -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))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment