diff --git a/bin/fakebmc b/bin/fakebmc new file mode 100644 index 00000000..0b20a7b1 --- /dev/null +++ b/bin/fakebmc @@ -0,0 +1,72 @@ +#!/usr/bin/env python +# Copyright 2015 Lenovo +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +__author__ = 'jjohnson2@lenovo.com' + +#this is a quick sample of how to write something that acts like a bmc +#to play: +# run fakebmc +## ipmitool -I lanplus -U admin -P password -H 127.0.0.1 power status +# Chassis Power is off +# # ipmitool -I lanplus -U admin -P password -H 127.0.0.1 power on +# Chassis Power Control: Up/On +# # ipmitool -I lanplus -U admin -P password -H 127.0.0.1 power status +# Chassis Power is on +# # ipmitool -I lanplus -U admin -P password -H 127.0.0.1 mc reset cold +# Sent cold reset command to MC +# (fakebmc exits) +import pyghmi.ipmi.bmc as bmc +import sys + + +class FakeBmc(bmc.Bmc): + def __init__(self, authdata): + super(FakeBmc, self).__init__(authdata) + self.powerstate = 'off' + self.bootdevice = 'default' + + def get_boot_device(self): + return self.bootdevice + + def set_boot_device(self, bootdevice): + self.bootdevice = bootdevice + + def cold_reset(self): + # Reset of the BMC, not managed system, here we will exit the demo + print 'shutting down in response to BMC cold reset request' + sys.exit(0) + + def get_power_state(self): + return self.powerstate + + def power_off(self): + # this should be power down without waiting for clean shutdown + self.powerstate = 'off' + print 'abruptly remove power' + + def power_on(self): + self.powerstate = 'on' + print 'powered on' + + def power_reset(self): + pass + + def power_shutdown(self): + # should attempt a clean shutdown + print 'politely shut down the system' + self.powerstate = 'off' + +if __name__ == '__main__': + mybmc = FakeBmc({'admin': 'password'}) + mybmc.listen() diff --git a/pyghmi/ipmi/bmc.py b/pyghmi/ipmi/bmc.py new file mode 100644 index 00000000..4606ebce --- /dev/null +++ b/pyghmi/ipmi/bmc.py @@ -0,0 +1,147 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2015 Lenovo +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__author__ = 'jjohnson2@lenovo.com' + +import pyghmi.ipmi.command as ipmicommand +import pyghmi.ipmi.private.serversession as serversession +import pyghmi.ipmi.private.session as ipmisession +import traceback + + +class Bmc(serversession.IpmiServer): + def cold_reset(self): + raise NotImplementedError + + def power_off(self): + raise NotImplementedError + + def power_on(self): + raise NotImplementedError + + def power_cycle(self): + raise NotImplementedError + + def power_reset(self): + raise NotImplementedError + + def power_shutdown(self): + raise NotImplementedError + + def get_power_state(self): + raise NotImplementedError + + @staticmethod + def handle_missing_command(session): + session.send_ipmi_response(code=0xc1) + + def get_chassis_status(self, session): + try: + powerstate = self.get_power_state() + except NotImplementedError: + return session.send_ipmi_response(code=0xc1) + if powerstate in ipmicommand.power_states: + powerstate = ipmicommand.power_states[powerstate] + if powerstate not in (0, 1): + raise Exception('BMC implementation mistake') + statusdata = [powerstate, 0, 0] + session.send_ipmi_response(data=statusdata) + + def control_chassis(self, request, session): + rc = 0 + try: + directive = request['data'][0] + if directive == 0: + rc = self.power_off() + elif directive == 1: + rc = self.power_on() + elif directive == 2: + rc = self.power_cycle() + elif directive == 3: + rc = self.power_reset() + elif directive == 5: + rc = self.power_shutdown() + if rc is None: + rc = 0 + session.send_ipmi_response(code=rc) + except NotImplementedError: + session.send_ipmi_response(code=0xcc) + + def get_boot_device(self): + raise NotImplementedError + + def get_system_boot_options(self, request, session): + if request['data'][0] == 5: # boot flags + try: + bootdevice = self.get_boot_device() + except NotImplementedError: + session.send_ipmi_response(data=[1, 5, 0, 0, 0, 0, 0]) + if (type(bootdevice) != int and + bootdevice in ipmicommand.boot_devices): + bootdevice = ipmicommand.boot_devices[bootdevice] + paramdata = [1, 5, 0b10000000, bootdevice, 0, 0, 0] + return session.send_ipmi_response(data=paramdata) + else: + session.send_ipmi_response(code=0x80) + + def set_boot_device(self, bootdevice): + raise NotImplementedError + + def set_system_boot_options(self, request, session): + if request['data'][0] in (0, 3, 4): + # for now, just smile and nod at boot flag bit clearing + # implementing it is a burden and implementing it does more to + # confuse users than serve a useful purpose + session.send_ipmi_response() + elif request['data'][0] == 5: + bootdevice = (request['data'][2] >> 2) & 0b1111 + try: + bootdevice = ipmicommand.boot_devices[bootdevice] + except KeyError: + session.send_ipmi_response(code=0xcc) + return + self.set_boot_device(bootdevice) + session.send_ipmi_response() + else: + raise NotImplementedError + + def handle_raw_request(self, request, session): + try: + if request['netfn'] == 6: + if request['command'] == 1: # get device id + return self.send_device_id(session) + elif request['command'] == 2: # cold reset + return session.send_ipmi_response(code=self.cold_reset()) + elif request['netfn'] == 0: + if request['command'] == 1: # get chassis status + return self.get_chassis_status(session) + elif request['command'] == 2: # chassis control + return self.control_chassis(request, session) + elif request['command'] == 8: # set boot options + return self.set_system_boot_options(request, session) + elif request['command'] == 9: # get boot options + return self.get_system_boot_options(request, session) + session.send_ipmi_response(code=0xc1) + except NotImplementedError: + session.send_ipmi_response(code=0xc1) + except Exception: + session._send_ipmi_net_payload(code=0xff) + traceback.print_exc() + + @classmethod + def listen(cls): + while True: + ipmisession.Session.wait_for_rsp(30) diff --git a/pyghmi/ipmi/command.py b/pyghmi/ipmi/command.py index 628cb47f..aa9e755b 100644 --- a/pyghmi/ipmi/command.py +++ b/pyghmi/ipmi/command.py @@ -42,6 +42,7 @@ boot_devices = { 3: 'safe', 5: 'optical', 6: 'setup', + 15: 'floppy', 0: 'default' } diff --git a/pyghmi/ipmi/private/serversession.py b/pyghmi/ipmi/private/serversession.py new file mode 100644 index 00000000..cb8c2540 --- /dev/null +++ b/pyghmi/ipmi/private/serversession.py @@ -0,0 +1,344 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2015 Lenovo +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This represents the server side of a session object +# Split into a separate file to avoid overly manipulating the as-yet +# client-centered session object +import hashlib +import hmac +import os +import pyghmi.ipmi.private.constants as constants +import pyghmi.ipmi.private.session as ipmisession +import struct +import uuid + + +class ServerSession(ipmisession.Session): + def __new__(cls, authdata, kg, clientaddr, netsocket, request, uuid, + bmc): + # Need to do default new type behavior. The normal session + # takes measures to assure the caller shares even when they + # didn't try. We don't have that operational mode to contend + # with in the server case (one file descriptor per bmc) + return object.__new__(cls) + + def create_open_session_response(self, request): + clienttag = ord(request[0]) + # role = request[1] + self.clientsessionid = list(struct.unpack('4B', request[4:8])) + # TODO(jbjohnso): intelligently handle integrity/auth/conf + #for now, forcibly do cipher suite 3 + self.managedsessionid = list(struct.unpack('4B', os.urandom(4))) + #table 13-17, 1 for now (hmac-sha1), 3 should also be supported + #table 13-18, integrity, 1 for now is hmac-sha1-96, 4 is sha256 + #confidentiality: 1 is aes-cbc-128, the only one + self.privlevel = 4 + response = ([clienttag, 0, self.privlevel, 0] + + self.clientsessionid + self.managedsessionid + + [ + 0, 0, 0, 8, 1, 0, 0, 0, # auth + 1, 0, 0, 8, 1, 0, 0, 0, # integrity + 2, 0, 0, 8, 1, 0, 0, 0, # privacy + ]) + return response + + def __init__(self, authdata, kg, clientaddr, netsocket, request, uuid, + bmc): + # begin conversation per RMCP+ open session request + self.uuid = uuid + self.rqaddr = constants.IPMI_BMC_ADDRESS + self.authdata = authdata + self.servermode = True + self.ipmiversion = 2.0 + self.sequencenumber = 0 + self.sessionid = 0 + self.bmc = bmc + self.lastpayload = None + self.broken = False + self.authtype = 6 + self.integrityalgo = 0 + self.confalgo = 0 + self.kg = kg + self.socket = netsocket + self.sockaddr = clientaddr + ipmisession.Session.bmc_handlers[clientaddr] = self + response = self.create_open_session_response(request) + self.send_payload(response, + constants.payload_types['rmcpplusopenresponse'], + retry=False) + + def _got_rmcp_openrequest(self, data): + response = self.create_open_session_response( + struct.pack('B' * len(data), *data)) + self.send_payload(response, + constants.payload_types['rmcpplusopenresponse'], + retry=False) + + def _got_rakp1(self, data): + clienttag = data[0] + self.Rm = data[8:24] + self.rolem = data[24] + self.maxpriv = self.rolem & 0b111 + namepresent = data[27] + if namepresent == 0: + #ignore null username for now + return + usernamebytes = data[28:] + self.username = struct.pack('%dB' % len(usernamebytes), *usernamebytes) + if self.username not in self.authdata: + # don't think about invalid usernames for now + return + uuidbytes = self.uuid.bytes + uuidbytes = list(struct.unpack('%dB' % len(uuidbytes), uuidbytes)) + self.uuiddata = uuidbytes + self.Rc = list(struct.unpack('16B', os.urandom(16))) + hmacdata = (self.clientsessionid + self.managedsessionid + + self.Rm + self.Rc + uuidbytes + + [self.rolem, len(self.username)]) + hmacdata = struct.pack('%dB' % len(hmacdata), *hmacdata) + hmacdata += self.username + self.kuid = self.authdata[self.username] + if self.kg is None: + self.kg = self.kuid + authcode = hmac.new( + self.kuid, hmacdata, hashlib.sha1).digest() + authcode = list(struct.unpack('%dB' % len(authcode), authcode)) + # regretably, ipmi mandates the server send out an hmac first + # akin to a leak of /etc/shadow, not too worrisome if the secret + # is complex, but terrible for most likely passwords selected by + # a human + newmessage = ([clienttag, 0, 0, 0] + self.clientsessionid + + self.Rc + uuidbytes + authcode) + self.send_payload(newmessage, constants.payload_types['rakp2'], + retry=False) + + def _got_rakp2(self, data): + # stub, server should not think about rakp2 + pass + + def _got_rakp3(self, data): + # for now drop rakp3 with bad authcode + # respond correctly a TODO(jjohnson2), since Kg being used + # yet incorrect is a scenario why rakp3 could be bad + # even if rakp2 was good + RmRc = struct.pack('B' * len(self.Rm + self.Rc), *(self.Rm + self.Rc)) + self.sik = hmac.new(self.kg, + RmRc + + struct.pack("2B", self.rolem, + len(self.username)) + + self.username, hashlib.sha1).digest() + self.k1 = hmac.new(self.sik, '\x01' * 20, hashlib.sha1).digest() + self.k2 = hmac.new(self.sik, '\x02' * 20, hashlib.sha1).digest() + self.aeskey = self.k2[0:16] + hmacdata = struct.pack('B' * len(self.Rc), *self.Rc) +\ + struct.pack("4B", *self.clientsessionid) +\ + struct.pack("2B", self.rolem, + len(self.username)) +\ + self.username + expectedauthcode = hmac.new(self.kuid, hmacdata, hashlib.sha1).digest() + authcode = struct.pack("%dB" % len(data[8:]), *data[8:]) + if expectedauthcode != authcode: + #TODO(jjohnson2): RMCP error back at invalid rakp3 + return + clienttag = data[0] + if data[1] != 0: + # client did not like our response, so ignore the rakp3 + return + self.localsid = struct.unpack(' 1: + if pendingpriv > self.maxpriv: + returncode = 0x81 + else: + self.clientpriv = request['data'][0] + self._send_ipmi_net_payload(code=returncode, + data=[self.clientpriv]) + elif request['netfn'] == 6 and request['command'] == 0x3c: + self.send_ipmi_response() + self.close_server_session() + else: + self.bmc.handle_raw_request(request, self) + + def close_server_session(self): + pass + + def _send_rakp4(self, tagvalue, statuscode): + payload = [tagvalue, statuscode, 0, 0] + self.clientsessionid + hmacdata = self.Rm + self.managedsessionid + self.uuiddata + hmacdata = struct.pack('%dB' % len(hmacdata), *hmacdata) + authdata = hmac.new(self.sik, hmacdata, hashlib.sha1).digest()[:12] + payload += struct.unpack('%dB' % len(authdata), authdata) + self.send_payload(payload, constants.payload_types['rakp4'], + retry=False) + self.confalgo = 'aes' + self.integrityalgo = 'sha1' + self.sessionid = struct.unpack( + '> 2 + mylun = netfnlun & 0b11 + if netfn == 6: # application request + if data[19] == '\x38': # cmd = get channel auth capabilities + verchannel, level = struct.unpack('2B', data[20:22]) + version = verchannel & 0b10000000 + if version != 0b10000000: + return + channel = verchannel & 0b1111 + if channel != 0xe: + return + (clientaddr, clientlun) = struct.unpack('BB', data[17:19]) + level &= 0b1111 + self.send_auth_cap(myaddr, mylun, clientaddr, clientlun, + sockaddr) + + def set_kg(self, kg): + """Sets the Kg for the BMC to use + + In RAKP, Kg is a BMC-specific integrity key that can be set. If not + set, Kuid is used for the integrity key + """ + try: + self.kg = kg.encode('utf-8') + except AttributeError: + self.kg = kg + + def send_device_id(self, session): + response = [self.deviceid, self.revision, self.firmwaremajor, + self.firmwareminor, self.ipmiversion, + self.additionaldevices] + response += struct.unpack('4B', struct.pack('> 2) + 1 + self.clientcommand = payload[5] + self._parse_payload(payload) + return entry = (payload[1] >> 2, payload[4], payload[5]) if self._lookup_request_entry(entry): self._remove_request_entry(entry) @@ -1353,8 +1401,9 @@ class Session(object): self.expectednetfn = 0x1ff # bigger than one byte means it can never # match the one byte value by mistake self.expectedcmd = 0x1ff - self.seqlun += 4 # prepare seqlun for next transmit - self.seqlun &= 0xff # when overflowing, wrap around + if not self.servermode: + self.seqlun += 4 # prepare seqlun for next transmit + self.seqlun &= 0xff # when overflowing, wrap around Session.waiting_sessions.pop(self, None) self.lastpayload = None # render retry mechanism utterly incapable of # doing anything, though it shouldn't matter @@ -1365,11 +1414,15 @@ class Session(object): # ^^ remove header of rsaddr/netfn/lun/checksum/rq/seq/lun del payload[-1] # remove the trailing checksum response['command'] = payload[0] - response['code'] = payload[1] - del payload[0:2] - response['data'] = payload + if self.servermode: + del payload[0:1] + response['data'] = payload + else: + response['code'] = payload[1] + del payload[0:2] + response['data'] = payload self.timeout = initialtimeout + (0.5 * random.random()) - if len(self.pendingpayloads) > 0: + if not self.servermode and len(self.pendingpayloads) > 0: (nextpayload, nextpayloadtype, retry) = \ self.pendingpayloads.popleft() self.send_payload(payload=nextpayload,