diff --git a/bin/fakebmc b/bin/fakebmc index 318970ef..2647f116 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 5dabe080..ce4a942e 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 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() diff --git a/pyghmi/ipmi/bmc.py b/pyghmi/ipmi/bmc.py index 4d6c23bd..dd3d68a3 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__ = '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) diff --git a/pyghmi/ipmi/console.py b/pyghmi/ipmi/console.py index 90ce2c88..3f87b6d3 100644 --- a/pyghmi/ipmi/console.py +++ b/pyghmi/ipmi/console.py @@ -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 diff --git a/pyghmi/ipmi/private/serversession.py b/pyghmi/ipmi/private/serversession.py index dbbe1430..ee8cea69 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))