diff --git a/ipmi/command.py b/ipmi/command.py index 2850a55e..de91796d 100644 --- a/ipmi/command.py +++ b/ipmi/command.py @@ -1,27 +1,22 @@ -""" -@author: Jarrod Johnson +# vim: tabstop=4 shiftwidth=4 softtabstop=4 -Copyright 2013 IBM Corporation +# Copyright 2013 IBM Corporation +# +# 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 low layer message framing portion of IPMI -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 +from ipmi.private import session - 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. -""" -from ipmi.private.session import Session, call_with_optional_args -def _raiseorcall(callback,response,args=None): - if callback is None: - if 'error' in response: - raise Exception(response['error']) - else: - call_with_optional_args(callback,args) boot_devices = { 'net': 4, @@ -48,21 +43,32 @@ power_states = { "reset": 3, "softoff": 5, "shutdown": 5, - "boot": -1, #not a valid direct boot state, but here for convenience of 'in' statement + # NOTE(jbjohnso): -1 is not a valid direct boot state, + # but here for convenience of 'in' statement + "boot": -1, } - + + +def _raiseorcall(callback, response, args=None): + if callback is None: + if 'error' in response: + raise Exception(response['error']) + else: + session.call_with_optional_args(callback, args) + + class Command(object): """Send IPMI commands to BMCs. - - This object represents a persistent session to an IPMI device (bmc) and + + This object represents a persistent session to an IPMI device (bmc) and allows the caller to reuse a single session to issue multiple commands. - This class can be used in a synchronous (wait for answer and return) or - asynchronous fashion (return immediately and provide responses by + This class can be used in a synchronous (wait for answer and return) or + asynchronous fashion (return immediately and provide responses by callbacks). Synchronous mode is the default behavior. - For asynchronous mode, simply pass in a callback function. It is - recommended to pass in an instance method to callback and ignore the - callback_args parameter. However, callback_args can optionally be populated + For asynchronous mode, simply pass in a callback function. It is + recommended to pass in an instance method to callback and ignore the + callback_args parameter. However, callback_args can optionally be populated if desired. :param bmc: hostname or ip address of the BMC @@ -71,44 +77,44 @@ class Command(object): :param kg: Optional parameter to use if BMC has a particular Kg configured """ - def __init__(self,bmc,userid,password,kg=None): - #TODO(jbjohnso): accept tuples and lists of each parameter for mass - #operations without pushing the async complexities up the stack - self.ipmi_session=Session(bmc=bmc, - userid=userid, - password=password, - kg=kg) + def __init__(self, bmc, userid, password, kg=None): + # TODO(jbjohnso): accept tuples and lists of each parameter for mass + # operations without pushing the async complexities up the stack + self.ipmi_session = session.Session(bmc=bmc, + userid=userid, + password=password, + kg=kg) - def get_bootdev(self,callback=None,callback_args=None): + def get_bootdev(self, callback=None, callback_args=None): """Get current boot device override information. Provides the current requested boot device. Be aware that not all IPMI devices support this. Even in BMCs that claim to, occasionally the BIOS - or UEFI fail to honor it. This is usually only applicable to the next + or UEFI fail to honor it. This is usually only applicable to the next reboot. - + :param callback: optional callback - :param callback_args: optional arguments to callback + :param callback_args: optional arguments to callback :returns: dict or True -- If callback is not provided, the response will be provided in the return """ - self.commandcallback=callback - self.commandcallbackargs=callback_args + self.commandcallback = callback + self.commandcallbackargs = callback_args self.ipmi_session.raw_command(netfn=0, command=9, - data=(5,0,0), + data=(5, 0, 0), callback=self._got_bootdev) return self._waitifsync() def _waitifsync(self): - self.requestpending=True + self.requestpending = True if self.commandcallback is None: while self.requestpending: - Session.wait_for_rsp() + session.Session.wait_for_rsp() return self.lastresponse return True - - def set_power(self,powerstate,wait=False,callback=None,callback_args=None): + + def set_power(self, powerstate, wait=False, callback=None, callback_args=None): """Request power state change :param powerstate: @@ -121,67 +127,70 @@ class Command(object): :param wait: If True, do not return or callback until system actually completes requested state change :param callback: optional callback - :param callback_args: optional arguments to callback + :param callback_args: optional arguments to callback :returns: dict or True -- If callback is not provided, the response """ - self.commandcallback=callback - self.commandcallbackargs=callback_args + self.commandcallback = callback + self.commandcallbackargs = callback_args if powerstate not in power_states: _raiseorcall(self.commandcallback, - {'error': - "Unknown power state %s requested"%powerstate}, + {'error': + "Unknown power state %s requested" % powerstate}, self.commandcallbackargs) - self.newpowerstate=powerstate - self.wait_for_power=wait + self.newpowerstate = powerstate + self.wait_for_power = wait self.ipmi_session.raw_command(netfn=0, command=1, callback=self._set_power_with_chassis_info - ) + ) return self._waitifsync() - def _set_power_with_chassis_info(self,response): + def _set_power_with_chassis_info(self, response): if 'error' in response: - _raiseorcall(self.commandcallback,response,self.commandcallbackargs) + _raiseorcall( + self.commandcallback, response, self.commandcallbackargs) return self.powerstate = 'on' if (response['data'][0] & 1) else 'off' - if self.newpowerstate=='boot': - self.newpowerstate = 'on' if self.powerstate=='off' else 'reset' + if self.newpowerstate == 'boot': + self.newpowerstate = 'on' if self.powerstate == 'off' else 'reset' self.ipmi_session.raw_command(netfn=0, command=2, data=[power_states[self.newpowerstate]], callback=self._power_set) - def _power_set(self,response): + def _power_set(self, response): if 'error' in response: - _raiseorcall(self.commandcallback,response,self.commandcallbackargs) + _raiseorcall( + self.commandcallback, response, self.commandcallbackargs) return - self.lastresponse={'pendingpowerstate': self.newpowerstate} - if (self.wait_for_power and - self.newpowerstate in ('on','off','shutdown','softoff')): - if self.newpowerstate in ('softoff','shutdown'): - self.waitpowerstate='off' + self.lastresponse = {'pendingpowerstate': self.newpowerstate} + if (self.wait_for_power and + self.newpowerstate in ('on', 'off', 'shutdown', 'softoff')): + if self.newpowerstate in ('softoff', 'shutdown'): + self.waitpowerstate = 'off' else: - self.waitpowerstate=self.newpowerstate + self.waitpowerstate = self.newpowerstate self.ipmi_session.raw_command(netfn=0, command=1, callback=self._power_wait) else: - self.requestpending=False + self.requestpending = False if self.commandcallback: - call_with_optional_args(self.commandcallback, + session.call_with_optional_args(self.commandcallback, self.lastresponse, self.commandcallbackargs) - def _power_wait(self,response): + def _power_wait(self, response): if 'error' in response: - _raiseorcall(self.commandcallback,response,self.commandcallbackargs) + _raiseorcall( + self.commandcallback, response, self.commandcallbackargs) return self.powerstate = 'on' if (response['data'][0] & 1) else 'off' - if self.powerstate==self.waitpowerstate: - self.requestpending=False - self.lastresponse={'powerstate': self.powerstate} + if self.powerstate == self.waitpowerstate: + self.requestpending = False + self.lastresponse = {'powerstate': self.powerstate} if self.commandcallback: - call_with_optional_args(self.commandcallback, + session.call_with_optional_args(self.commandcallback, self.lastresponse, self.commandcallbackargs) return @@ -211,51 +220,53 @@ class Command(object): In practice, this flag not being set does not preclude UEFI boot on any system I've encountered. :param callback: optional callback - :param callback_args: optional arguments to callback + :param callback_args: optional arguments to callback :returns: dict or True -- If callback is not provided, the response """ - self.commandcallback=callback - self.commandcallbackargs=callback_args + self.commandcallback = callback + self.commandcallbackargs = callback_args if bootdev not in boot_devices: _raiseorcall(self.commandcallback, - {'error': "Unknown bootdevice %s requested"%bootdev}, - self.commandcallbackargs) - self.bootdev=boot_devices[bootdev] - self.persistboot=persist - self.uefiboot=uefiboot - #first, we disable timer by way of set system boot options, - #then move on to set chassis capabilities - self.requestpending=True - #Set System Boot Options is netfn=0, command=8, data + {'error': "Unknown bootdevice %s requested" % + bootdev}, + self.commandcallbackargs) + self.bootdev = boot_devices[bootdev] + self.persistboot = persist + self.uefiboot = uefiboot + # first, we disable timer by way of set system boot options, + # then move on to set chassis capabilities + self.requestpending = True + # Set System Boot Options is netfn=0, command=8, data self.ipmi_session.raw_command(netfn=0, - command=8,data=(3,8), + command=8, data=(3, 8), callback=self._bootdev_timer_disabled) if callback is None: while self.requestpending: - Session.wait_for_rsp() + session.Session.wait_for_rsp() return self.lastresponse - def _bootdev_timer_disabled(self,response): - self.requestpending=False - self.lastresponse=response + def _bootdev_timer_disabled(self, response): + self.requestpending = False + self.lastresponse = response if 'error' in response: - _raiseorcall(self.commandcallback,response,self.commandcallbackargs) + _raiseorcall( + self.commandcallback, response, self.commandcallbackargs) return - bootflags=0x80 + bootflags = 0x80 if self.uefiboot: - bootflags = bootflags | 1<<5 + bootflags = bootflags | 1 << 5 if self.persistboot: - bootflags = bootflags | 1<<6 - if self.bootdev==0: - bootflags=0 - data=(5,bootflags,self.bootdev,0,0,0) + bootflags = bootflags | 1 << 6 + if self.bootdev == 0: + bootflags = 0 + data = (5, bootflags, self.bootdev, 0, 0, 0) self.ipmi_session.raw_command(netfn=0, command=8, data=data, callback=self.commandcallback, callback_args=self.commandcallbackargs) - + def raw_command(self, netfn, command, @@ -273,85 +284,73 @@ class Command(object): :param command: Command value :param data: Command data as a tuple or list :param callback: optional callback - :param callback_args: optional arguments to callback + :param callback_args: optional arguments to callback :returns: dict or True -- If callback is not provided, the response """ - response=self.ipmi_session.raw_command(netfn=0, - command=1, - callback=callback, - callback_args=callback_args) - if response: #this means there was no callback + response = self.ipmi_session.raw_command(netfn=0, + command=1, + callback=callback, + callback_args=callback_args) + if response: # this means there was no callback if 'error' in response: raise Exception(response['error']) return response - def _got_bootdev(self,response): - #interpret response per 'get system boot options' - self.requestpending=False + + def _got_bootdev(self, response): + # interpret response per 'get system boot options' + self.requestpending = False if 'error' in response: - _raiseorcall(self.commandcallback,response,self.commandcallbackargs) + _raiseorcall( + self.commandcallback, response, self.commandcallbackargs) return - #this should only be invoked for get system boot option complying to - #ipmi spec and targeting the 'boot flags' parameter - assert (response['command'] == 9 and - response['netfn'] == 1 and - response['data'][0]==1 and - (response['data'][1]&0b1111111)==5) - if (response['data'][1] & 0b10000000 or - not response['data'][2] & 0b10000000): - self.lastresponse={ 'bootdev': 'default' } - else: #will consult data2 of the boot flags parameter for the data + # this should only be invoked for get system boot option complying to + # ipmi spec and targeting the 'boot flags' parameter + assert (response['command'] == 9 and + response['netfn'] == 1 and + response['data'][0] == 1 and + (response['data'][1] & 0b1111111) == 5) + if (response['data'][1] & 0b10000000 or + not response['data'][2] & 0b10000000): + self.lastresponse = {'bootdev': 'default'} + else: # will consult data2 of the boot flags parameter for the data bootnum = (response['data'][3] & 0b111100) >> 2 bootdev = boot_devices[bootnum] if (bootdev): - self.lastresponse={'bootdev': bootdev} + self.lastresponse = {'bootdev': bootdev} else: - self.lastresponse={'bootdev': bootnum} + self.lastresponse = {'bootdev': bootnum} if self.commandcallback: - call_with_optional_args(self.commandcallback, + session.call_with_optional_args(self.commandcallback, self.lastresponse, self.commandcallbackargs) - - def get_power(self,callback=None,callback_args=None): - """ - Get current power state of the managed system - The response, if successful, should contain 'powerstate' key and + def get_power(self, callback=None, callback_args=None): + """Get current power state of the managed system + + The response, if successful, should contain 'powerstate' key and either 'on' or 'off' to indicate current state. :param callback: optional callback - :param callback_args: optional arguments to callback + :param callback_args: optional arguments to callback :returns: dict or True -- If callback is not provided, the response """ - self.commandcallback=callback - self.commandcallbackargs=callback_args + self.commandcallback = callback + self.commandcallbackargs = callback_args self.ipmi_session.raw_command(netfn=0, command=1, callback=self._got_power) return self._waitifsync() - def _got_power(self,response): - self.requestpending=False + def _got_power(self, response): + self.requestpending = False if 'error' in response: - _raiseorcall(self.commandcallback,response,self.commandcallbackargs) + _raiseorcall( + self.commandcallback, response, self.commandcallbackargs) return assert(response['command'] == 1 and response['netfn'] == 1) self.powerstate = 'on' if (response['data'][0] & 1) else 'off' - self.lastresponse={'powerstate': self.powerstate} + self.lastresponse = {'powerstate': self.powerstate} if self.commandcallback: - call_with_optional_args(self.commandcallback, + session.call_with_optional_args(self.commandcallback, self.lastresponse, self.commandcallbackargs) - -if __name__ == "__main__": - import sys - import os - ipmicmd = ipmi_command(bmc=sys.argv[1], - userid=sys.argv[2], - password=os.environ['IPMIPASS']) - print ipmicmd.get_power() - print ipmicmd.set_power('on',wait=True) - print ipmicmd.get_bootdev() - print ipmicmd.set_bootdev('network') - print ipmicmd.get_bootdev() - print ipmicmd.set_bootdev('default') - print ipmicmd.get_bootdev() diff --git a/ipmi/private/constants.py b/ipmi/private/constants.py index b3e13f4f..dd1c48de 100644 --- a/ipmi/private/constants.py +++ b/ipmi/private/constants.py @@ -1,25 +1,23 @@ -""" -@author: Jarrod Johnson +# vim: tabstop=4 shiftwidth=4 softtabstop=4 -Copyright 2013 IBM Corporation - -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. -""" +# Copyright 2013 IBM Corporation +# +# 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. payload_types = { 'ipmi': 0x0, - 'sol' : 0x1, + 'sol': 0x1, 'rmcpplusopenreq': 0x10, 'rmcpplusopenresponse': 0x11, 'rakp1': 0x12, @@ -29,50 +27,51 @@ payload_types = { } rmcp_codes = { - 1: 'Insufficient resources to create new session (wait for existing sessions to timeout)', - 2: 'Invalid Session ID', - 3: 'Invalid payload type', - 4: 'Invalid authentication algorithm', - 5: 'Invalid integrity algorithm', - 6: 'No matching integrity payload', - 7: 'No matching integrity payload', - 8: 'Inactive Session ID', - 9: 'Invalid role', - 0xa: 'Unauthorized role or privilege level requested', - 0xb: 'Insufficient resources tocreate a session at the requested role', - 0xc: 'Invalid username length', - 0xd: 'Unauthorized name', - 0xe: 'Unauthorized GUID', - 0xf: 'Invalid integrity check value', - 0x10: 'Invalid confidentiality algorithm', - 0x11: 'No Cipher suite match with proposed security algorithms', - 0x12: 'Illegal or unrecognized parameter', + 1: ("Insufficient resources to create new session (wait for existing " + "sessions to timeout)"), + 2: "Invalid Session ID", + 3: "Invalid payload type", + 4: "Invalid authentication algorithm", + 5: "Invalid integrity algorithm", + 6: "No matching integrity payload", + 7: "No matching integrity payload", + 8: "Inactive Session ID", + 9: "Invalid role", + 0xa: "Unauthorized role or privilege level requested", + 0xb: "Insufficient resources tocreate a session at the requested role", + 0xc: "Invalid username length", + 0xd: "Unauthorized name", + 0xe: "Unauthorized GUID", + 0xf: "Invalid integrity check value", + 0x10: "Invalid confidentiality algorithm", + 0x11: "No Cipher suite match with proposed security algorithms", + 0x12: "Illegal or unrecognized parameter", } command_completion_codes = { - (7,0x39): { + (7, 0x39): { 0x81: "Invalid user name", 0x82: "Null user disabled", }, - (7,0x3a): { + (7, 0x3a): { 0x81: "No available login slots", 0x82: "No available login slots for requested user", 0x83: "No slot available with requested privilege level", 0x84: "Session sequence number out of range", 0x85: "Invalid session ID", - 0x86: "Requested privilege level exceeds requested user permissions on this channel", + 0x86: ("Requested privilege level exceeds requested user permissions " + "on this channel"), }, - (7,0x3b): { #Set session privilege level + (7, 0x3b): { # Set session privilege level 0x80: "User is not allowed requested priveleg level", 0x81: "Requested privilege level is not allowed over this channel", 0x82: "Cannot disable user level authentication", }, - (1,8): { #set system boot options + (1, 8): { # set system boot options 0x80: "Parameter not supported", 0x81: "Attempt to set set 'set in progress' when not 'set complete'", 0x82: "Attempt to write read-only parameter", } - } ipmi_completion_codes = { @@ -102,4 +101,3 @@ ipmi_completion_codes = { 0xd6: "Cannot execute command because subfunction disabled or unavailable", 0xff: "Unspecified", } - diff --git a/ipmi/private/session.py b/ipmi/private/session.py index cb12e5b9..bab7fb58 100644 --- a/ipmi/private/session.py +++ b/ipmi/private/session.py @@ -1,132 +1,150 @@ -""" -@author: Jarrod Johnson +# vim: tabstop=4 shiftwidth=4 softtabstop=4 -Copyright 2013 IBM Corporation - -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. -""" +# Copyright 2013 IBM Corporation +# +# 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 low layer message framing portion of IPMI + import atexit -from collections import deque -from hashlib import md5 +import collections +import hashlib import os -from random import random +import random import select import socket +import struct +import time -from struct import pack, unpack -from time import time from Crypto.Cipher import AES -from Crypto.Hash import HMAC, SHA +from Crypto.Hash import HMAC +from Crypto.Hash import SHA -from ipmi.private.constants import payload_types, ipmi_completion_codes, command_completion_codes, payload_types, rmcp_codes +from ipmi.private import constants -initialtimeout = 0.5 #minimum timeout for first packet to retry in any given - #session. This will be randomized to stagger out retries - #in case of congestion -def _aespad(data): # ipmi demands a certain pad scheme, per table 13-20 AES-CBC - # encrypted payload fields - newdata=list(data) - currlen=len(data)+1 #need to count the pad length field as well - neededpad=currlen%16 - if neededpad: #if it happens to be zero, hurray, but otherwise invert the - #sense of the padding - neededpad = 16-neededpad - padval=1 +initialtimeout = 0.5 # minimum timeout for first packet to retry in any given + # session. This will be randomized to stagger out retries + # in case of congestion + + +def _aespad(data): + """ipmi demands a certain pad scheme, + per table 13-20 AES-CBC encrypted payload fields. + """ + newdata = list(data) + currlen = len(data) + 1 # need to count the pad length field as well + neededpad = currlen % 16 + if neededpad: # if it happens to be zero, hurray, but otherwise invert the + # sense of the padding + neededpad = 16 - neededpad + padval = 1 while padval <= neededpad: newdata.append(padval) - padval+=1 + padval += 1 newdata.append(neededpad) return newdata - -''' -In order to simplify things, in a number of places there is a callback facility and optional arguments to pass in. -An OO oriented caller may find the additional argument needless. Allow them to ignore it by skipping the argument if None -''' -def call_with_optional_args(callback,*args): - newargs=[] + +def call_with_optional_args(callback, *args): + """In order to simplify things, in a number of places there is a callback + facility and optional arguments to pass in. An object-oriented caller may + find the additional argument needless. Allow them to ignore it by skipping + the argument if None. + """ + newargs = [] for arg in args: if arg is not None: newargs.append(arg) callback(*newargs) -def get_ipmi_error(response,suffix=""): +def get_ipmi_error(response, suffix=""): if 'error' in response: - return response['error']+suffix + return response['error'] + suffix + code = response['code'] - command = response['command'] - netfn = response['netfn'] if code == 0: return False - if ((netfn,command) in command_completion_codes and - code in command_completion_codes[(netfn,command)]): - return command_completion_codes[(netfn,command)][code]+suffix - elif code in ipmi_completion_codes: - return ipmi_completion_codes[code]+suffix + + command = response['command'] + netfn = response['netfn'] + + if ((netfn, command) in constants.command_completion_codes + and code in constants.command_completion_codes[(netfn, command)]): + res = constants.command_completion_codes[(netfn, command)][code] + res += suffix + elif code in constants.ipmi_completion_codes: + res = constants.ipmi_completion_codes[code] + suffix else: - return "Unknown code "+code+" encountered" + res = "Unknown code " + code + " encountered" + + return res + class Session: - poller=select.poll() - bmc_handlers={} - waiting_sessions={} - peeraddr_to_nodes={} - #Upon exit of python, make sure we play nice with BMCs by assuring closed - #sessions for all that we tracked + poller = select.poll() + bmc_handlers = {} + waiting_sessions = {} + peeraddr_to_nodes = {} + # Upon exit of python, make sure we play nice with BMCs by assuring closed + # sessions for all that we tracked + @classmethod def _cleanup(cls): for session in cls.bmc_handlers.itervalues(): session.logout() + @classmethod def _createsocket(cls): atexit.register(cls._cleanup) - cls.socket = socket.socket(socket.AF_INET6,socket.SOCK_DGRAM) #INET6 - #can do IPv4 if you are nice to it - try: #we will try to fixup our receive buffer size if we are smaller - #than allowed. + cls.socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) # INET6 + # can do IPv4 if you are nice to it + try: # we will try to fixup our receive buffer size if we are smaller + # than allowed. maxmf = open("/proc/sys/net/core/rmem_max") rmemmax = int(maxmf.read()) - rmemmax = rmemmax/2 - curmax=cls.socket.getsockopt(socket.SOL_SOCKET,socket.SO_RCVBUF) - curmax = curmax/2 + rmemmax = rmemmax / 2 + curmax = cls.socket.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF) + curmax = curmax / 2 if (rmemmax > curmax): cls.socket.setsockopt(socket.SOL_SOCKET, - socket.SO_RCVBUF, - rmemmax) - except: + socket.SO_RCVBUF, + rmemmax) + except Exception: + # FIXME: be more selective in catching exceptions pass - curmax=cls.socket.getsockopt(socket.SOL_SOCKET,socket.SO_RCVBUF) - cls.poller.register(cls.socket,select.POLLIN) - curmax = curmax/2 - #we throttle such that we never have no more outstanding packets than - #our receive buffer should be able to handle - cls.pending=0 - cls.maxpending=curmax/1000 #pessimistically assume 1 kilobyte messages, #way larger than almost all ipmi datagrams - #for faster performance, sysadmins may want to examine and tune - #/proc/sys/net/core/rmem_max up. This allows the module to request - #more, but does not increase buffers for applications that do less - #creative things - #TODO(jbjohnso): perhaps spread sessions across a socket pool when - #rmem_max is small, still get ~65/socket, but avoid long queues that - #might happen with low rmem_max and putting thousands of nodes in line - ''' - This function handles the synchronous caller case in liue of a client - provided callback - ''' - def _sync_login(self,response): + + curmax = cls.socket.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF) + cls.poller.register(cls.socket, select.POLLIN) + curmax = curmax / 2 + # we throttle such that we never have no more outstanding packets than + # our receive buffer should be able to handle + cls.pending = 0 + cls.maxpending = curmax / 1000 + # pessimistically assume 1 kilobyte messages, + # which is way larger than almost all ipmi datagrams. + # For faster performance, sysadmins may want to examine and tune + # /proc/sys/net/core/rmem_max up. This allows the module to request + # more, but does not increase buffers for applications that do less + # creative things + # TODO(jbjohnso): perhaps spread sessions across a socket pool when + # rmem_max is small, still get ~65/socket, but avoid long queues that + # might happen with low rmem_max and putting thousands of nodes in line + + def _sync_login(self, response): + """Handle synchronous callers in liue of + a client-provided callback. + """ if 'error' in response: raise Exception(response['error']) @@ -138,135 +156,150 @@ class Session: kg=None, onlogon=None, onlogonargs=None): - self.bmc=bmc - self.userid=userid - self.password=password - self.noretry=False - self.nowait=False + self.bmc = bmc + self.userid = userid + self.password = password + self.noretry = False + self.nowait = False if kg is not None: - self.kg=kg + self.kg = kg else: - self.kg=password - self.port=port - self.onlogonargs=onlogonargs + self.kg = password + self.port = port + self.onlogonargs = onlogonargs if (onlogon is None): - self.async=False - self.onlogon=self._sync_login + self.async = False + self.onlogon = self._sync_login else: - self.async=True - self.onlogon=onlogon - if not hasattr(Session,'socket'): + self.async = True + self.onlogon = onlogon + if not hasattr(Session, 'socket'): self._createsocket() self.login() if not self.async: while not self.logged: Session.wait_for_rsp() + def _initsession(self): - self.localsid=2017673555 #this number can be whatever we want. I picked - #'xCAT' minus 1 so that a hexdump of packet - # would show xCAT - self.privlevel=4 #for the moment, assume admin access - #TODO(jbjohnso): make flexible - self.confalgo=0 - self.aeskey=None - self.integrityalgo=0 - self.k1=None - self.rmcptag=1 - self.ipmicallback=None - self.ipmicallbackargs=None - self.sessioncontext=None - self.sequencenumber=0 - self.sessionid=0 - self.authtype=0 - self.ipmiversion=1.5 - self.timeout=initialtimeout+(0.5*random()) - self.seqlun=0 - self.rqaddr=0x81 #per IPMI table 5-4, software ids in the ipmi spec may - #be 0x81 through 0x8d. We'll stick with 0x81 for now, - #do not forsee a reason to adjust - self.logged=0 - self.sockaddr=None #when we confirm a working sockaddr, put it here to - #skip getaddrinfo - self.tabooseq={} #this tracks netfn,command,seqlun combinations that - #were retried so that we don't loop around and reuse - #the same request data and cause potential ambiguity - #in return - self.ipmi15only=0 #default to supporting ipmi 2.0. Strictly by spec, - #this should gracefully be backwards compat, but some - #1.5 implementations checked reserved bits - def _checksum(self,*data): #Two's complement over the data - csum=sum(data) - csum=csum^0xff - csum+=1 + # NOTE(jbjohnso): this number can be whatever we want. + # I picked 'xCAT' minus 1 so that a hexdump of packet + # would show xCAT + self.localsid = 2017673555 + + # NOTE(jbjohnso): for the moment, assume admin access + # TODO(jbjohnso): make flexible + self.privlevel = 4 + + self.confalgo = 0 + self.aeskey = None + self.integrityalgo = 0 + self.k1 = None + self.rmcptag = 1 + self.ipmicallback = None + self.ipmicallbackargs = None + self.sessioncontext = None + self.sequencenumber = 0 + self.sessionid = 0 + self.authtype = 0 + self.ipmiversion = 1.5 + self.timeout = initialtimeout + (0.5 * random.random()) + self.seqlun = 0 + # NOTE(jbjohnso): per IPMI table 5-4, software ids in the ipmi spec may + # be 0x81 through 0x8d. We'll stick with 0x81 for now, + # do not forsee a reason to adjust + self.rqaddr = 0x81 + + self.logged = 0 + # NOTE(jbjohnso): when we confirm a working sockaddr, put it here to + # skip getaddrinfo + self.sockaddr = None + # NOTE(jbjohnso): this tracks netfn,command,seqlun combinations that + # were retried so that we don't loop around and reuse + # the same request data and cause potential ambiguity + # in return + self.tabooseq = {} + # NOTE(jbjohnso): default to supporting ipmi 2.0. Strictly by spec, + # this should gracefully be backwards compat, but some + # 1.5 implementations checked reserved bits + self.ipmi15only = 0 + + def _checksum(self, *data): # Two's complement over the data + csum = sum(data) + csum = csum ^ 0xff + csum += 1 csum &= 0xff return csum - ''' - This function generates the core ipmi payload that would be applicable - for any channel (including KCS) - ''' - def _make_ipmi_payload(self,netfn,command,data=()): - self.expectedcmd=command - self.expectednetfn=netfn+1 #in ipmi, the response netfn is always one - #higher than the request payload, we assume - #we are always the requestor for now - seqincrement=7 #IPMI spec forbids gaps bigger then 7 in seq number. - #Risk the taboo rather than violate the rules - while ((netfn,command,self.seqlun) in self.tabooseq and - self.tabooseq[(netfn,command,self.seqlun)] and seqincrement): - self.tabooseq[(self.expectednetfn,command,self.seqlun)]-=1 - #Allow taboo to eventually expire after a few rounds - self.seqlun += 4 #the last two bits are lun, so add 4 to add 1 - self.seqlun &= 0xff #we only have one byte, wrap when exceeded - seqincrement-=1 - header=[0x20,netfn<<2] #figure 13-4, first two bytes are rsaddr and - #netfn, rsaddr is always 0x20 since we are - #addressing BMC - reqbody=[self.rqaddr,self.seqlun,command]+list(data) - headsum=self._checksum(*header) - bodysum=self._checksum(*reqbody) - payload=header+[headsum]+reqbody+[bodysum] + def _make_ipmi_payload(self, netfn, command, data=()): + """This function generates the core ipmi payload that would be + applicable for any channel (including KCS) + """ + self.expectedcmd = command + self.expectednetfn = netfn + \ + 1 # in ipmi, the response netfn is always one + # higher than the request payload, we assume + # we are always the requestor for now + seqincrement = 7 # IPMI spec forbids gaps bigger then 7 in seq number. + # Risk the taboo rather than violate the rules + while ((netfn, command, self.seqlun) in self.tabooseq and + self.tabooseq[(netfn, command, self.seqlun)] and seqincrement): + self.tabooseq[(self.expectednetfn, command, self.seqlun)] -= 1 + # Allow taboo to eventually expire after a few rounds + self.seqlun += 4 # the last two bits are lun, so add 4 to add 1 + self.seqlun &= 0xff # we only have one byte, wrap when exceeded + seqincrement -= 1 + header = [0x20, netfn << 2] + #figure 13-4, first two bytes are rsaddr and + # netfn, rsaddr is always 0x20 since we are + # addressing BMC + reqbody = [self.rqaddr, self.seqlun, command] + list(data) + headsum = self._checksum(*header) + bodysum = self._checksum(*reqbody) + payload = header + [headsum] + reqbody + [bodysum] return payload - def _generic_callback(self,response): + def _generic_callback(self, response): errorstr = get_ipmi_error(response) if errorstr: - response['error']=errorstr - self.lastresponse=response + response['error'] = errorstr + self.lastresponse = response + def raw_command(self, netfn, command, data=[], callback=None, callback_args=None): - self.ipmicallbackargs=callback_args + self.ipmicallbackargs = callback_args if callback is None: - self.lastresponse=None - self.ipmicallback=self._generic_callback + self.lastresponse = None + self.ipmicallback = self._generic_callback else: - self.ipmicallback=callback - self._send_ipmi_net_payload(netfn,command,data) + self.ipmicallback = callback + self._send_ipmi_net_payload(netfn, command, data) if callback is None: while self.lastresponse is None: Session.wait_for_rsp() return self.lastresponse - def _send_ipmi_net_payload(self,netfn,command,data): - ipmipayload=self._make_ipmi_payload(netfn,command,data) - payload_type = payload_types['ipmi'] + + def _send_ipmi_net_payload(self, netfn, command, data): + ipmipayload = self._make_ipmi_payload(netfn, command, data) + payload_type = constants.payload_types['ipmi'] if self.integrityalgo: - payload_type |= 0b01000000 + payload_type |= 0b01000000 if self.confalgo: - payload_type |= 0b10000000 - self._pack_payload(payload=ipmipayload,payload_type=payload_type) - def _pack_payload(self,payload=None,payload_type=None): + payload_type |= 0b10000000 + self._pack_payload(payload=ipmipayload, payload_type=payload_type) + + def _pack_payload(self, payload=None, payload_type=None): if payload is None: - payload=self.lastpayload + payload = self.lastpayload if payload_type is None: - payload_type=self.last_payload_type - message = [0x6,0,0xff,0x07] #constant RMCP header for IPMI + payload_type = self.last_payload_type + message = [0x6, 0, 0xff, 0x07] # constant RMCP header for IPMI baretype = payload_type & 0b00111111 - self.lastpayload=payload - self.last_payload_type=payload_type + self.lastpayload = payload + self.last_payload_type = payload_type message.append(self.authtype) if (self.ipmiversion == 2.0): message.append(payload_type) @@ -274,523 +307,550 @@ class Session: raise Exception("TODO(jbjohnso): OEM Payloads") elif (baretype == 1): raise Exception("TODO(jbjohnso): SOL Payload") - elif baretype not in payload_types.values(): - raise Exception("Unrecognized payload type %d"%baretype) - message += unpack("!4B",pack(">8); - iv=os.urandom(16) - message += list(unpack("16B",iv)) - payloadtocrypt=_aespad(payload) - crypter = AES.new(self.aeskey,AES.MODE_CBC,iv) - crypted = crypter.encrypt(pack("%dB"%len(payloadtocrypt), + message.append(newpsize & 0xff) + message.append(newpsize >> 8) + iv = os.urandom(16) + message += list(struct.unpack("16B", iv)) + payloadtocrypt = _aespad(payload) + crypter = AES.new(self.aeskey, AES.MODE_CBC, iv) + crypted = crypter.encrypt(struct.pack("%dB" % len(payloadtocrypt), *payloadtocrypt)) - crypted = list(unpack("%dB"%len(crypted),crypted)) + crypted = list(struct.unpack("%dB" % len(crypted), crypted)) message += crypted - else: #no confidetiality algorithm - message.append(psize&0xff) - message.append(psize>>8); + else: # no confidetiality algorithm + message.append(psize & 0xff) + message.append(psize >> 8) message += list(payload) - if self.integrityalgo: #see table 13-8, - #RMCP+ packet format - #TODO(jbjohnso): SHA256 which is now allowed - neededpad=(len(message)-2)%4 + if self.integrityalgo: # see table 13-8, + # RMCP+ packet format + # TODO(jbjohnso): SHA256 which is now + # allowed + neededpad = (len(message) - 2) % 4 if neededpad: - neededpad = 4-neededpad - message += [0xff]*neededpad + neededpad = 4 - neededpad + message += [0xff] * neededpad message.append(neededpad) - message.append(7) #reserved, 7 is the required value for the - #specification followed + message.append(7) # reserved, 7 is the required value for the + # specification followed integdata = message[4:] authcode = HMAC.new(self.k1, - pack("%dB"%len(integdata), + struct.pack("%dB" % len(integdata), *integdata), - SHA).digest()[:12] #SHA1-96 - #per RFC2404 truncates to 96 bits - message += unpack("12B",authcode) - self.netpacket = pack("!%dB"%len(message),*message) + SHA).digest()[:12] # SHA1-96 + # per RFC2404 truncates to 96 bits + message += struct.unpack("12B", authcode) + self.netpacket = struct.pack("!%dB" % len(message), *message) self._xmit_packet() - def _ipmi15authcode(self,payload,checkremotecode=False): - if self.authtype == 0: #Only for things prior to auth in ipmi 1.5, not - #like 2.0 cipher suite 0 + def _ipmi15authcode(self, payload, checkremotecode=False): + if self.authtype == 0: # Only for things prior to auth in ipmi 1.5, not + # like 2.0 cipher suite 0 return () password = self.password padneeded = 16 - len(password) if padneeded < 0: raise Exception("Password is too long for ipmi 1.5") - password += '\x00'*padneeded - passdata = unpack("16B",password) + password += '\x00' * padneeded + passdata = struct.unpack("16B", password) if checkremotecode: - seqbytes = unpack("!4B",pack(" 16: raise Exception("Username too long for IPMI, must not exceed 16") - padneeded=16-len(self.userid) - userid=self.userid+('\x00'*padneeded) - reqdata += unpack("!16B",userid) - self.ipmicallback=self._got_session_challenge - self._send_ipmi_net_payload(netfn=0x6,command=0x39,data=reqdata) + padneeded = 16 - len(self.userid) + userid = self.userid + ('\x00' * padneeded) + reqdata += struct.unpack("!16B", userid) + self.ipmicallback = self._got_session_challenge + self._send_ipmi_net_payload(netfn=0x6, command=0x39, data=reqdata) def _open_rmcpplus_request(self): - self.authtype=6 - self.localsid+=1 #have unique local session ids to ignore aborted login - #attempts from the past - self.rmcptag+=1 + self.authtype = 6 + self.localsid += 1 # have unique local session ids to ignore aborted login + # attempts from the past + self.rmcptag += 1 data = [ self.rmcptag, - 0, #request as much privilege as the channel will give us - 0,0, #reserved - ] - data += list(unpack("4B",pack(">2 != self.expectednetfn or - payload[5] != self.expectedcmd): - return -1 #this payload is not a match for our outstanding packet - if hasattr(self,'hasretried') and self.hasretried: - self.hasretried=0 - self.tabooseq[(self.expectednetfn,self.expectedcmd,self.seqlun)]=16 - #try to skip it for at most 16 cycles of overflow - #We want to now remember that we do not have an expected packet - self.expectednetfn=0x1ff #bigger than one byte means it can never match - self.expectedcmd=0x1ff - self.seqlun += 4 #prepare seqlun for next transmit - self.seqlun &= 0xff #when overflowing, wrap around + + def _parse_ipmi_payload(self, payload): + # For now, skip the checksums since we are in LAN only, + # TODO(jbjohnso): if implementing other channels, add checksum checks + # here + if (payload[4] != self.seqlun or payload[1] >> 2 != self.expectednetfn or + payload[5] != self.expectedcmd): + return -1 # this payload is not a match for our outstanding packet + if hasattr(self, 'hasretried') and self.hasretried: + self.hasretried = 0 + self.tabooseq[ + (self.expectednetfn, self.expectedcmd, self.seqlun)] = 16 + # try to skip it for at most 16 cycles of overflow + # We want to now remember that we do not have an expected packet + self.expectednetfn = 0x1ff # bigger than one byte means it can never match + self.expectedcmd = 0x1ff + self.seqlun += 4 # prepare seqlun for next transmit + self.seqlun &= 0xff # when overflowing, wrap around del Session.waiting_sessions[self] - self.lastpayload=None #render retry mechanism utterly incapable of doing - #anything, though it shouldn't matter - self.last_payload_type=None - response={} - response['netfn'] = payload[1]>>2 - del payload[0:5] # 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] + self.lastpayload = None # render retry mechanism utterly incapable of doing + # anything, though it shouldn't matter + self.last_payload_type = None + response = {} + response['netfn'] = payload[1] >> 2 + del payload[0:5] + # 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 - self.timeout=initialtimeout+(0.5*random()) + response['data'] = payload + self.timeout = initialtimeout + (0.5 * random.random()) call_with_optional_args(self.ipmicallback, response, self.ipmicallbackargs) @@ -798,94 +858,96 @@ class Session: def _timedout(self): if not self.lastpayload: return - self.nowait=True + self.nowait = True self.timeout += 1 if self.noretry: return if self.timeout > 5: - response={'error': 'timeout'} + response = {'error': 'timeout'} call_with_optional_args(self.ipmicallback, response, self.ipmicallbackargs) - self.nowait=False + self.nowait = False return elif self.sessioncontext == 'FAILED': - self.nowait=False + self.nowait = False return if self.sessioncontext == 'OPENSESSION': - #In this case, we want to craft a new session request to have - #unambiguous session id regardless of how packet was dropped or - #delayed in this case, it's safe to just redo the request + # In this case, we want to craft a new session request to have + # unambiguous session id regardless of how packet was dropped or + # delayed in this case, it's safe to just redo the request self._open_rmcpplus_request() - elif (self.sessioncontext == 'EXPECTINGRAKP2' or - self.sessioncontext == 'EXPECTINGRAKP4'): - #If we can't be sure which RAKP was dropped or that RAKP3/4 was just - #delayed, the most reliable thing to do is rewind and start over - #bmcs do not take kindly to receiving RAKP1 or RAKP3 twice + elif (self.sessioncontext == 'EXPECTINGRAKP2' or + self.sessioncontext == 'EXPECTINGRAKP4'): + # If we can't be sure which RAKP was dropped or that RAKP3/4 was just + # delayed, the most reliable thing to do is rewind and start over + # bmcs do not take kindly to receiving RAKP1 or RAKP3 twice self._relog() - else: #in IPMI case, the only recourse is to act as if the packet is - #idempotent. SOL has more sophisticated retry handling - #the biggest risks are reset sp, which is often fruitless to retry - #and chassis reset, which sometimes will shoot itself - #systematically in the head in a shared port case making replies - #impossible - self.hasretried=1 #remember so that we can track taboo combinations - #of sequence number, netfn, and lun due to - #ambiguity on the wire + else: # in IPMI case, the only recourse is to act as if the packet is + # idempotent. SOL has more sophisticated retry handling + # the biggest risks are reset sp, which is often fruitless to retry + # and chassis reset, which sometimes will shoot itself + # systematically in the head in a shared port case making replies + # impossible + self.hasretried = 1 # remember so that we can track taboo combinations + # of sequence number, netfn, and lun due to + # ambiguity on the wire self._pack_payload() - self.nowait=False + self.nowait = False + def _xmit_packet(self): - if not self.nowait: #if we are retrying, we really need to get the - #packet out and get our timeout updated - Session.wait_for_rsp(timeout=0) #take a convenient opportunity - #to drain the socket queue if - #applicable + if not self.nowait: # if we are retrying, we really need to get the + # packet out and get our timeout updated + Session.wait_for_rsp(timeout=0) # take a convenient opportunity + # to drain the socket queue if + # applicable while Session.pending > Session.maxpending: Session.wait_for_rsp() - Session.waiting_sessions[self]={} - Session.waiting_sessions[self]['ipmisession']=self - Session.waiting_sessions[self]['timeout']=self.timeout+time() - Session.pending+=1 + Session.waiting_sessions[self] = {} + Session.waiting_sessions[self]['ipmisession'] = self + Session.waiting_sessions[self]['timeout'] = self.timeout + time() + Session.pending += 1 if self.sockaddr: - Session.socket.sendto(self.netpacket,self.sockaddr) - else: #he have not yet picked a working sockaddr for this connection, - #try all the candidates that getaddrinfo provides + Session.socket.sendto(self.netpacket, self.sockaddr) + else: # he have not yet picked a working sockaddr for this connection, + # try all the candidates that getaddrinfo provides for res in socket.getaddrinfo(self.bmc, self.port, 0, socket.SOCK_DGRAM): sockaddr = res[4] - if (res[0] == socket.AF_INET): #convert the sockaddr to AF_INET6 - newhost='::ffff:'+sockaddr[0] - sockaddr = (newhost,sockaddr[1],0,0) - Session.bmc_handlers[sockaddr]=self - Session.socket.sendto(self.netpacket,sockaddr) - if self.sequencenumber: #seq number of zero will be left alone as it is - #special, otherwise increment + if (res[0] == socket.AF_INET): # convert the sockaddr to AF_INET6 + newhost = '::ffff:' + sockaddr[0] + sockaddr = (newhost, sockaddr[1], 0, 0) + Session.bmc_handlers[sockaddr] = self + Session.socket.sendto(self.netpacket, sockaddr) + if self.sequencenumber: # seq number of zero will be left alone as it is + # special, otherwise increment self.sequencenumber += 1 - def logout(self,callback=None,callback_args=None): + + def logout(self, callback=None, callback_args=None): if not self.logged: if callback is None: - return {'success': True } - callback({'success': True }) + return {'success': True} + callback({'success': True}) return - self.noretry=True #risk open sessions if logout request gets dropped, - #logout is not idempotent so this is the better bet + self.noretry = True # risk open sessions if logout request gets dropped, + # logout is not idempotent so this is the better bet self.raw_command(command=0x3c, netfn=6, - data=unpack("4B",pack("I",self.sessionid)), + data=struct.unpack("4B", struct.pack("I", self.sessionid)), callback=callback, callback_args=callback_args) - self.logged=0 + self.logged = 0 if callback is None: - return {'success': True } - callback({'success': True }) + return {'success': True} + callback({'success': True}) if __name__ == "__main__": import sys ipmis = Session(bmc=sys.argv[1], - userid=sys.argv[2], - password=os.environ['IPMIPASS']) - print ipmis.raw_command(command=2,data=[1],netfn=0) - print get_ipmi_error({'command':8,'code':128,'netfn':1}) + userid=sys.argv[2], + password=os.environ['IPMIPASS']) + print ipmis.raw_command(command=2, data=[1], netfn=0) + print get_ipmi_error({'command': 8, 'code': 128, 'netfn': 1}) diff --git a/ipmictl.py b/ipmictl.py index cd08b00c..7d1aea21 100755 --- a/ipmictl.py +++ b/ipmictl.py @@ -24,22 +24,22 @@ import os import sys from ipmi.command import Command -password=os.environ['IPMIPASSWORD'] -os.environ['IPMIPASSWORD']="" +password = os.environ['IPMIPASSWORD'] +os.environ['IPMIPASSWORD'] = "" if (len(sys.argv) < 3): print "Usage:" - print " IPMIPASSWORD=password %s bmc username "%sys.argv[0] + print " IPMIPASSWORD=password %s bmc username " % sys.argv[0] sys.exit(1) -bmc=sys.argv[1] -userid=sys.argv[2] -command=sys.argv[3] -arg=None -if len(sys.argv)==5: - arg=sys.argv[4] -ipmicmd = Command(bmc=bmc,userid=userid,password=password) +bmc = sys.argv[1] +userid = sys.argv[2] +command = sys.argv[3] +arg = None +if len(sys.argv) == 5: + arg = sys.argv[4] +ipmicmd = Command(bmc=bmc, userid=userid, password=password) if command == 'power': if arg: - print ipmicmd.set_power(arg,wait=True) + print ipmicmd.set_power(arg, wait=True) else: print ipmicmd.get_power() elif command == 'bootdev': @@ -47,4 +47,3 @@ elif command == 'bootdev': print ipmicmd.set_bootdev(arg) else: print ipmicmd.get_bootdev() -