Last active
July 10, 2017 07:41
-
-
Save yosshy/d0b96e387d5dafec9def05f20a492181 to your computer and use it in GitHub Desktop.
SOL capability for IPMI server in pyghmi
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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