diff --git a/confluent_server/aiohmi/__init__.py b/confluent_server/aiohmi/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/confluent_server/aiohmi/cmd/__init__.py b/confluent_server/aiohmi/cmd/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/confluent_server/aiohmi/cmd/fakebmc.py b/confluent_server/aiohmi/cmd/fakebmc.py new file mode 100755 index 00000000..ec24e3e8 --- /dev/null +++ b/confluent_server/aiohmi/cmd/fakebmc.py @@ -0,0 +1,97 @@ +# 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 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 argparse +import sys + +import aiohmi.ipmi.bmc as bmc + + +class FakeBmc(bmc.Bmc): + def __init__(self, authdata, port): + super(FakeBmc, self).__init__(authdata, port) + 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' + + def is_active(self): + return self.powerstate == 'on' + + def iohandler(self, data): + print(data) + if self.sol: + self.sol.send_data(data) + + +def main(): + parser = argparse.ArgumentParser( + prog='fakebmc', + description='Pretend to be a BMC', + ) + parser.add_argument('--port', + dest='port', + type=int, + default=623, + help='Port to listen on; defaults to 623') + args = parser.parse_args() + mybmc = FakeBmc({'admin': 'password'}, port=args.port) + mybmc.listen() + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/confluent_server/aiohmi/cmd/pyghmicons.py b/confluent_server/aiohmi/cmd/pyghmicons.py new file mode 100755 index 00000000..590ff3b5 --- /dev/null +++ b/confluent_server/aiohmi/cmd/pyghmicons.py @@ -0,0 +1,88 @@ +# 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. + +""" A simple little script to exemplify/test ipmi.console module """ + +import fcntl +import os +import select +import sys +import termios +import threading +import tty + + +from aiohmi.ipmi import console + + +def _doinput(sol): + while True: + select.select((sys.stdin,), (), (), 600) + try: + data = sys.stdin.read() + except (IOError, OSError) as e: + if e.errno == 11: + continue + raise + + sol.send_data(data) + + +def _print(data): + bailout = False + if not isinstance(data, str): + bailout = True + data = repr(data) + sys.stdout.write(data) + sys.stdout.flush() + if bailout: + raise Exception(data) + + +def main(): + tcattr = termios.tcgetattr(sys.stdin) + newtcattr = tcattr + # TODO(jbjohnso): add our exit handler + newtcattr[-1][termios.VINTR] = 0 + newtcattr[-1][termios.VSUSP] = 0 + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, newtcattr) + + tty.setraw(sys.stdin.fileno()) + currfl = fcntl.fcntl(sys.stdin.fileno(), fcntl.F_GETFL) + fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, currfl | os.O_NONBLOCK) + + try: + if sys.argv[3] is None: + passwd = os.environ['IPMIPASSWORD'] + else: + passwd_file = sys.argv[3] + with open(passwd_file, "r") as f: + passwd = f.read() + + sol = console.Console(bmc=sys.argv[1], userid=sys.argv[2], + password=passwd, iohandler=_print, force=True) + inputthread = threading.Thread(target=_doinput, args=(sol,)) + inputthread.daemon = True + inputthread.start() + sol.main_loop() + + except Exception: + currfl = fcntl.fcntl(sys.stdin.fileno(), fcntl.F_GETFL) + fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, currfl ^ os.O_NONBLOCK) + termios.tcsetattr(sys.stdin, termios.TCSANOW, tcattr) + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/confluent_server/aiohmi/cmd/pyghmiutil.py b/confluent_server/aiohmi/cmd/pyghmiutil.py new file mode 100755 index 00000000..2231d406 --- /dev/null +++ b/confluent_server/aiohmi/cmd/pyghmiutil.py @@ -0,0 +1,92 @@ +# 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 is an example of using the library in a synchronous fashion. For now, +it isn't conceived as a general utility to actually use, just help developers +understand how the ipmi_command class workes. +""" + +import functools +import os +import sys + +from aiohmi.ipmi import command + + +def docommand(args, result, ipmisession): + command = args[0] + args = args[1:] + print("Logged into %s" % ipmisession.bmc) + if 'error' in result: + print(result['error']) + return + if command == 'power': + if args: + print(ipmisession.set_power(args[0], wait=True)) + else: + value = ipmisession.get_power() + print("%s: %s" % (ipmisession.bmc, value['powerstate'])) + elif command == 'bootdev': + if args: + print(ipmisession.set_bootdev(args[0])) + else: + print(ipmisession.get_bootdev()) + elif command == 'sensors': + for reading in ipmisession.get_sensor_data(): + print(reading) + elif command == 'health': + print(ipmisession.get_health()) + elif command == 'inventory': + for item in ipmisession.get_inventory(): + print(item) + elif command == 'leds': + for led in ipmisession.get_leds(): + print(led) + elif command == 'graphical': + print(ipmisession.get_graphical_console()) + elif command == 'net': + print(ipmisession.get_net_configuration()) + elif command == 'raw': + print(ipmisession.raw_command( + netfn=int(args[0]), + command=int(args[1]), + data=map(lambda x: int(x, 16), args[2:]))) + + +def main(): + if (len(sys.argv) < 3) or 'IPMIPASSWORD' not in os.environ: + print("Usage:") + print(" IPMIPASSWORD=password %s bmc username " % + sys.argv[0]) + return 1 + + password = os.environ['IPMIPASSWORD'] + os.environ['IPMIPASSWORD'] = "" + bmc = sys.argv[1] + userid = sys.argv[2] + + bmcs = bmc.split(',') + ipmicmd = None + for bmc in bmcs: + # NOTE(etingof): is it right to have `ipmicmd` overridden? + ipmicmd = command.Command( + bmc=bmc, userid=userid, password=password, + onlogon=functools.partial(docommand, sys.argv[3:])) + + if ipmicmd: + ipmicmd.eventloop() + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/confluent_server/aiohmi/cmd/virshbmc.py b/confluent_server/aiohmi/cmd/virshbmc.py new file mode 100755 index 00000000..787db83c --- /dev/null +++ b/confluent_server/aiohmi/cmd/virshbmc.py @@ -0,0 +1,161 @@ +# 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 is a simple, but working proof of concept of using aiohmi.ipmi.bmc to +control a VM +""" + +import argparse +import sys +import threading + +import libvirt + +import aiohmi.ipmi.bmc as bmc + + +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, console): + try: + data = console.stream.recv(1024) + except Exception: + return + if console.sol: + console.sol.send_data(data) + + +class LibvirtBmc(bmc.Bmc): + """A class to provide an IPMI interface to the VirtualBox APIs.""" + + def __init__(self, authdata, hypervisor, domain, port): + 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 = False + self.conn.domainEventRegister(lifecycle_callback, self) + self.sol_thread = None + + 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): + if self.domain.isActive(): + return 'on' + else: + return 'off' + + def power_off(self): + if not self.domain.isActive(): + return 0xd5 # Not valid in this state + self.domain.destroy() + + def power_on(self): + if self.domain.isActive(): + return 0xd5 # Not valid in this state + self.domain.create() + + def power_reset(self): + if not self.domain.isActive(): + return 0xd5 # Not valid in this state + self.domain.reset() + + def power_shutdown(self): + if not self.domain.isActive(): + return 0xd5 # Not valid in this state + self.domain.shutdown() + + def is_active(self): + return self.domain.isActive() + + def check_console(self): + if (self.state[0] == libvirt.VIR_DOMAIN_RUNNING + or self.state[0] == libvirt.VIR_DOMAIN_PAUSED): + if self.stream is None: + self.stream = self.conn.newStream(libvirt.VIR_STREAM_NONBLOCK) + self.domain.openConsole(None, self.stream, 0) + self.stream.eventAddCallback(libvirt.VIR_STREAM_EVENT_READABLE, + stream_callback, self) + else: + if self.stream: + self.stream.eventRemoveCallback() + self.stream = None + + return self.run_console + + def activate_payload(self, request, session): + super(LibvirtBmc, self).activate_payload(request, session) + self.run_console = True + self.sol_thread = threading.Thread(target=self.loop) + self.sol_thread.start() + + def deactivate_payload(self, request, session): + self.run_console = False + self.sol_thread.join() + super(LibvirtBmc, self).deactivate_payload(request, session) + + def iohandler(self, data): + if self.stream: + self.stream.send(data) + + def loop(self): + while self.check_console(): + libvirt.virEventRunDefaultImpl() + + +def main(): + parser = argparse.ArgumentParser( + prog='virshbmc', + description='Pretend to be a BMC and proxy to virsh', + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + parser.add_argument('--port', + dest='port', + type=int, + default=623, + help='(UDP) port to listen on') + parser.add_argument('--connect', + dest='hypervisor', + default='qemu:///system', + help='The hypervisor to connect to') + parser.add_argument('--domain', + dest='domain', + required=True, + help='The name of the domain to manage') + args = parser.parse_args() + + libvirt.virEventRegisterDefaultImpl() + libvirt.registerErrorHandler(error_handler, None) + + mybmc = LibvirtBmc({'admin': 'password'}, + hypervisor=args.hypervisor, + domain=args.domain, + port=args.port) + mybmc.listen() + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/confluent_server/aiohmi/constants.py b/confluent_server/aiohmi/constants.py new file mode 100644 index 00000000..0a60960c --- /dev/null +++ b/confluent_server/aiohmi/constants.py @@ -0,0 +1,18 @@ +# Copyright 2014 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. + + +class Health(object): + Ok = 0 + Warning, Critical, Failed = [2**x for x in range(0, 3)] diff --git a/confluent_server/aiohmi/exceptions.py b/confluent_server/aiohmi/exceptions.py new file mode 100644 index 00000000..3c6427ef --- /dev/null +++ b/confluent_server/aiohmi/exceptions.py @@ -0,0 +1,74 @@ +# Copyright 2013 IBM Corporation +# Copyright 2015-2017 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. +# +# The Exceptions that Pyghmi can throw + + +class PyghmiException(Exception): + pass + + +class IpmiException(PyghmiException): + def __init__(self, text='', code=0): + super(IpmiException, self).__init__(text) + self.ipmicode = code + + +class RedfishError(PyghmiException): + def __init__(self, text='', msgid=None): + super(RedfishError, self).__init__(text) + self.msgid = msgid + + +class UnrecognizedCertificate(Exception): + def __init__(self, text='', certdata=None): + super(UnrecognizedCertificate, self).__init__(text) + self.certdata = certdata + + +class TemporaryError(Exception): + # A temporary condition that should clear, but warrants reporting to the + # caller + pass + + +class InvalidParameterValue(PyghmiException): + pass + + +class BmcErrorException(IpmiException): + # This denotes when library detects an invalid BMC behavior + pass + + +class UnsupportedFunctionality(PyghmiException): + # Indicates when functionality is requested that is not supported by + # current endpoint + pass + + +class BypassGenericBehavior(PyghmiException): + # Indicates that an OEM handler wants to abort any standards based + # follow up + pass + + +class FallbackData(PyghmiException): + # Indicates the OEM handler has data to be used if the generic + # check comes up empty + def __init__(self, fallbackdata): + self.fallbackdata = fallbackdata + + pass diff --git a/confluent_server/aiohmi/ipmi/__init__.py b/confluent_server/aiohmi/ipmi/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/confluent_server/aiohmi/ipmi/bmc.py b/confluent_server/aiohmi/ipmi/bmc.py new file mode 100644 index 00000000..92e55968 --- /dev/null +++ b/confluent_server/aiohmi/ipmi/bmc.py @@ -0,0 +1,198 @@ +# 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. + +import struct +import traceback + +import aiohmi.ipmi.command as ipmicommand +import aiohmi.ipmi.console as console +import aiohmi.ipmi.private.serversession as serversession +import aiohmi.ipmi.private.session as ipmisession + + +__author__ = 'jjohnson2@lenovo.com' + + +class Bmc(serversession.IpmiServer): + + activated = False + sol = None + iohandler = None + + def get_system_guid(self): + raise NotImplementedError + + 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 pulse_diag(self): + raise NotImplementedError + + def power_shutdown(self): + raise NotImplementedError + + 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 + solport = list(struct.unpack('BB', struct.pack('!H', self.port))) + session.send_ipmi_response( + data=[0, 0, 0, 0, 1, 0, 1, 0] + solport + [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) + + 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 == 4: + # i.e. Pulse a diagnostic interrupt(NMI) directly + rc = self.pulse_diag() + 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['command'] == 0x37: # get system guid + guid = self.get_system_guid() + return session.send_ipmi_response(code=0x00, data=guid.bytes_le) + 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) + 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, timeout=30): + while True: + ipmisession.Session.wait_for_rsp(timeout) diff --git a/confluent_server/aiohmi/ipmi/command.py b/confluent_server/aiohmi/ipmi/command.py new file mode 100644 index 00000000..32d6ab1d --- /dev/null +++ b/confluent_server/aiohmi/ipmi/command.py @@ -0,0 +1,2314 @@ +# Copyright 2013 IBM Corporation +# Copyright 2015-2017 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 low layer message framing portion of IPMI""" + +from itertools import chain +import os +import socket +import struct +import threading + +import asyncio +import aiohmi.constants as const +import aiohmi.exceptions as exc +import aiohmi.ipmi.events as sel +import aiohmi.ipmi.fru as fru +import aiohmi.ipmi.oem.generic as genericoem +from aiohmi.ipmi.oem.lookup import get_oem_handler +import aiohmi.ipmi.private.util as util +from aiohmi.ipmi import sdr + + +try: + from aiohmi.ipmi.private import session +except ImportError: + session = None +try: + from aiohmi.ipmi.private import localsession +except ImportError: + localsession = None + +try: + range = xrange +except NameError: + pass +try: + buffer +except NameError: + buffer = memoryview + +boot_devices = { + 'net': 4, + 'network': 4, + 'pxe': 4, + 'hd': 8, + 'safe': 0xc, + 'cd': 0x14, + 'cdrom': 0x14, + 'optical': 0x14, + 'dvd': 0x14, + 'floppy': 0x3c, + 'usb': 0x3c, + 'default': 0x0, + 'setup': 0x18, + 'bios': 0x18, + 'f1': 0x18, + 1: 'network', + 2: 'hd', + 3: 'safe', + 5: 'optical', + 6: 'setup', + 15: 'floppy', + 0: 'default' +} + +power_states = { + "off": 0, + "on": 1, + "reset": 3, + "diag": 4, + "softoff": 5, + "shutdown": 5, + # NOTE(jbjohnso): -1 is not a valid direct boot state, + # but here for convenience of 'in' statement + "boot": -1, +} + + +def select_simplesession(): + global session + import aiohmi.ipmi.private.simplesession as session + + +def _mask_to_cidr(mask): + maskn = struct.unpack_from('>I', mask)[0] + cidr = 32 + while maskn & 0b1 == 0 and cidr > 0: + cidr -= 1 + maskn >>= 1 + return cidr + + +def _cidr_to_mask(prefix): + return struct.pack('>I', 2 ** prefix - 1 << (32 - prefix)) + + +class Housekeeper(threading.Thread): + """A Maintenance thread for housekeeping + + Long lived use of aiohmi may warrant some recurring asynchronous behavior. + This stock thread provides a simple minimal context for these housekeeping + tasks to run in. To use, do 'aiohmi.ipmi.command.Maintenance().start()' + and from that point forward, aiohmi should execute any needed ongoing + tasks automatically as needed. This is an alternative to calling + wait_for_rsp or eventloop in a thread of the callers design. + """ + + def run(self): + Command.eventloop() + + +class Command(object): + """Send IPMI commands to BMCs. + + 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 + 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 + if desired. + + :param bmc: hostname or ip address of the BMC (default is local) + :param userid: username to use to connect (default to no user) + :param password: password to connect to the BMC (defaults to no password) + :param onlogon: function to run when logon completes in an asynchronous + fashion. This will result in a greenthread behavior. + :param kg: Optional parameter to use if BMC has a particular Kg configured + :param verifycallback: For OEM extensions that use HTTPS, this function + will be used to evaluate the certificate. + :param keepalive: If False, then an idle connection will logout rather + than keepalive unless held open by console or ongoing + activity. + """ + + @classmethod + async def create(cls, bmc=None, userid=None, password=None, port=623, + kg=None, privlevel=None, verifycallback=None, + keepalive=True, **kwargs): + self = cls() + # TODO(jbjohnso): accept tuples and lists of each parameter for mass + # operations without pushing the async complexities up the stack + self.bmc = bmc + self._sdrcachedir = None + self._sdr = None + self._oem = None + self._oemknown = False + self._netchannel = None + self._ipv6support = None + self.certverify = verifycallback + self.kwargs = kwargs + if bmc is None: + self.ipmi_session = localsession.Session() + else: + self.ipmi_session = await session.Session(bmc=self.bmc, + userid=userid, + password=password, + port=port, + kg=kg, + privlevel=privlevel, + keepalive=keepalive) + # induce one iteration of the loop, now that we would be + # prepared for it in theory + await session.Session.wait_for_rsp(0) + return self + + def set_sdr_cachedir(self, path): + """Register use of a directory for SDR cache. + + Takes the given directory and uses it to persist SDR cache run to run. + This can greatly improve performance across runs. + + :param path: + :return: + """ + self._sdrcachedir = path + + def register_key_handler(self, callback, type='tls'): + """Assign a verification handler for a public key + + When the library attempts to communicate with the management target + using a non-IPMI protocol, it will try to verify a key. This + allows a caller to register a key handler for accepting or rejecting + a public key/certificate. The callback will be passed the peer public + key or certificate. + + :param callback: The function to call with public key/certificate + :param type: Whether the callback is meant to handle 'tls' or 'ssh', + defaults to 'tls' + """ + if type == 'tls': + self.certverify = callback + + @classmethod + async def eventloop(cls): + while True: + await session.Session.wait_for_rsp() + + @classmethod + async def wait_for_rsp(cls, timeout): + """Delay for no longer than timeout for next response. + + This acts like a sleep that exits on activity. + + :param timeout: Maximum number of seconds before returning + """ + return await session.Session.wait_for_rsp(timeout=timeout) + + async def _get_device_id(self): + response = await self.raw_command(netfn=0x06, command=0x01) + return { + 'device_id': response['data'][0], + 'device_revision': response['data'][1] & 0b1111, + 'manufacturer_id': struct.unpack( + '> 4 & 0b1111, + response['data'][3] & 0b1111) + } + + async def oem_init(self): + """Initialize the command object for OEM capabilities + + A number of capabilities are either totally OEM defined or + else augmented somehow by knowledge of the OEM. This + method does an interrogation to identify the OEM. + + """ + if self._oemknown: + return + if self.bmc is None: + self._oem = await genericoem.OEMHandler.create(None, None) + self._oemknown = True + return + self._oem, self._oemknown = await get_oem_handler(await self._get_device_id(), + self) + + async def get_bootdev(self): + """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 reboot. + + :raises: IpmiException on an error. + :returns: dict --The response will be provided in the return as a dict + """ + response = await self.raw_command(netfn=0, command=9, data=(5, 0, 0)) + # interpret response per 'get system boot options' + # 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): + return {'bootdev': 'default', 'persistent': True} + else: # will consult data2 of the boot flags parameter for the data + persistent = False + uefimode = False + if response['data'][2] & 0b1000000: + persistent = True + if response['data'][2] & 0b100000: + uefimode = True + bootnum = (response['data'][3] & 0b111100) >> 2 + bootdev = boot_devices.get(bootnum) + if bootdev: + return {'bootdev': bootdev, + 'persistent': persistent, + 'uefimode': uefimode} + else: + return {'bootdev': bootnum, + 'persistent': persistent, + 'uefimode': uefimode} + + async def reseat_bay(self, bay): + """Request the reseat of a bay + + Request the enclosure manager to reseat the system in a particular + bay. + + :param bay: The bay identifier to reseat + :return: + """ + await self.oem_init() + await self._oem.reseat_bay(bay) + + async def set_power(self, powerstate, wait=False, bridge_request=None): + """Request power state change (helper) + + :param powerstate: + * on -- Request system turn on + * off -- Request system turn off without waiting + for OS to shutdown + * shutdown -- Have system request OS proper + shutdown + * reset -- Request system reset without waiting for + OS + * boot -- If system is off, then 'on', else 'reset' + :param wait: If True, do not return until system actually completes + requested state change for 300 seconds. + If a non-zero number, adjust the wait time to the + requested number of seconds + :param bridge_request: The target slave address and channel number for + the bridge request. + :raises: IpmiException on an error + :returns: dict -- A dict describing the response retrieved + """ + await self.oem_init() + + if hasattr(self._oem, 'set_power'): + return self._oem.set_power(powerstate, + bridge_request=bridge_request) + + if hasattr(self._oem, 'process_power_state'): + powerstate = self._oem.process_power_state( + powerstate, bridge_request=bridge_request) + + if powerstate not in power_states: + raise exc.InvalidParameterValue( + "Unknown power state %s requested" % powerstate) + newpowerstate = powerstate + oldpowerstate = await self._get_power_state(bridge_request=bridge_request) + if oldpowerstate == newpowerstate: + return {'powerstate': oldpowerstate} + if newpowerstate == 'boot': + newpowerstate = 'on' if oldpowerstate == 'off' else 'reset' + response = await self.raw_command( + netfn=0, command=2, data=[power_states[newpowerstate]], + bridge_request=bridge_request) + lastresponse = {'pendingpowerstate': newpowerstate} + waitattempts = 300 + if not isinstance(wait, bool): + waitattempts = wait + if wait and newpowerstate in ('on', 'off', 'shutdown', 'softoff'): + if newpowerstate in ('softoff', 'shutdown'): + waitpowerstate = 'off' + else: + waitpowerstate = newpowerstate + currpowerstate = None + while currpowerstate != waitpowerstate and waitattempts > 0: + await asyncio.sleep(1) + currpowerstate = await self._get_power_state( + bridge_request=bridge_request) + waitattempts -= 1 + if currpowerstate != waitpowerstate: + raise exc.IpmiException( + "System did not accomplish power state change") + return {'powerstate': currpowerstate} + else: + return lastresponse + + async def _get_power_state(self, bridge_request=None): + response = await self.raw_command(netfn=0, command=1, + bridge_request=bridge_request) + assert (response['command'] == 1 and response['netfn'] == 1) + curr_power_state = 'on' if (response['data'][0] & 1) else 'off' + return curr_power_state + + async def get_video_launchdata(self): + """Get data required to launch a remote video session to target. + + This is a highly proprietary scenario, the return data may vary greatly + host to host. The return should be a dict describing the type of data + and the data. For example {'jnlp': jnlpstring} + """ + await self.oem_init() + return await self._oem.get_video_launchdata() + + async def reset_bmc(self): + """Do a cold reset in BMC""" + response = await self.raw_command(netfn=6, command=2, retry=False) + if response and 'error' in response: + raise exc.IpmiException(response['error']) + + async def set_bootdev(self, + bootdev, + persist=False, + uefiboot=False): + """Set boot device to use on next reboot (helper) + + :param bootdev: + *network -- Request network boot + *hd -- Boot from hard drive + *safe -- Boot from hard drive, requesting 'safe mode' + *optical -- boot from CD/DVD/BD drive + *setup -- Boot into setup utility + *default -- remove any IPMI directed boot device + request + :param persist: If true, ask that system firmware use this device + beyond next boot. Be aware many systems do not honor + this + :param uefiboot: If true, request UEFI boot explicitly. Strictly + speaking, the spec sugests that if not set, the system + should BIOS boot and offers no "don't care" option. + In practice, this flag not being set does not preclude + UEFI boot on any system I've encountered. + :raises: IpmiException on an error. + :returns: dict or True -- If callback is not provided, the response + """ + if bootdev not in boot_devices: + return {'error': "Unknown bootdevice %s requested" % bootdev} + bootdevnum = boot_devices[bootdev] + # first, we disable timer by way of set system boot options, + # then move on to set chassis capabilities + # Set System Boot Options is netfn=0, command=8, data + response = await self.raw_command(netfn=0, command=8, data=(3, 8)) + if 'error' in response: + raise exc.IpmiException(response['error']) + bootflags = 0x80 + if uefiboot: + bootflags |= 1 << 5 + if persist: + bootflags |= 1 << 6 + if bootdevnum == 0: + bootflags = 0 + data = (5, bootflags, bootdevnum, 0, 0, 0) + response = await self.raw_command(netfn=0, command=8, data=data) + if 'error' in response: + raise exc.IpmiException(response['error']) + return {'bootdev': bootdev} + + async def xraw_command(self, **kwargs): + raise Exception("Don't use this anymore") + return await self.raw_command(**kwargs) + + async def raw_command(self, netfn, command, bridge_request=(), data=(), + retry=True, timeout=None, rslun=0): + """Send raw ipmi command to BMC, raising exception on error + + This is identical to raw_command, except it raises exceptions + on IPMI errors and returns data as a buffer. This is the recommend + function to use. The response['data'] being a buffer allows + traditional indexed access as well as works nicely with + struct.unpack_from when certain data is coming back. + + :param netfn: Net function number + :param command: Command value + :param bridge_request: The target slave address and channel number for + the bridge request. + :param data: Command data as a tuple or list + :param retry: Whether to retry this particular payload or not, defaults + to true. + :param timeout: A custom time to wait for initial reply, useful for + a slow command. This may interfere with retry logic. + :returns: dict -- The response from IPMI device + """ + rsp = await self.ipmi_session.raw_command(netfn=netfn, command=command, + bridge_request=bridge_request, + data=data, + retry=retry, timeout=timeout, + rslun=rslun) + if rsp and 'error' in rsp: + raise exc.IpmiException(rsp['error'], rsp['code']) + if rsp and 'data' in rsp: + rsp['data'] = buffer(rsp['data']) + return rsp + + async def get_diagnostic_data(self, savefile, progress=None, autosuffix=False): + if os.path.exists(savefile) and not os.path.isdir(savefile): + raise exc.InvalidParameterValue( + 'Not allowed to overwrite existing file: {0}'.format( + savefile)) + await self.oem_init() + return await self._oem.get_diagnostic_data(savefile, progress, autosuffix) + + async def get_description(self): + """Get physical attributes for the system, e.g. for GUI use + + :returns: dict -- dict containing attributes, 'height' is for + how many U tall, 'slot' for what slot in a blade enclosure + or 0 if not blade, for example. + """ + await self.oem_init() + return await self._oem.get_description() + + async def oldraw_command(self, netfn, command, bridge_request=(), data=(), + retry=True, timeout=None, rslun=0): + """Send raw ipmi command to BMC + + This allows arbitrary IPMI bytes to be issued. This is commonly used + for certain vendor specific commands. + + Example: ipmicmd.raw_command(netfn=0,command=4,data=(5)) + + :param netfn: Net function number + :param command: Command value + :param bridge_request: The target slave address and channel number for + the bridge request. + :param data: Command data as a tuple or list + :param retry: Whether or not to retry command if no response received. + Defaults to True + :param timeout: A custom amount of time to wait for initial reply + :returns: dict -- The response from IPMI device + """ + rsp = await self.ipmi_session.raw_command(netfn=netfn, command=command, + bridge_request=bridge_request, + data=data, + retry=retry, timeout=timeout, + rslun=rslun) + return rsp + + async def get_power(self, bridge_request=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 bridge_request: The target slave address and channel number for + the bridge request. + :returns: dict -- {'powerstate': value} + """ + await self.oem_init() + if hasattr(self._oem, 'get_power'): + return {'powerstate': await self._oem.get_power( + bridge_request=bridge_request)} + + return {'powerstate': await self._get_power_state( + bridge_request=bridge_request)} + + async def set_identify(self, on=True, duration=None, blink=False): + """Request identify light + + Request the identify light to turn off, on for a duration, + or on indefinitely. Other than error exceptions, + + :param on: Set to True to force on or False to force off + :param duration: Set if wanting to request turn on for a duration + rather than indefinitely on + """ + await self.oem_init() + try: + await self._oem.set_identify(on, duration, blink) + return + except exc.BypassGenericBehavior: + return + except exc.UnsupportedFunctionality: + pass + if blink: + raise exc.IpmiException('Blink not supported with generic IPMI') + if duration is not None: + duration = int(duration) + if duration > 255: + duration = 255 + if duration < 0: + duration = 0 + response = await self.raw_command(netfn=0, command=4, data=[duration]) + return + forceon = 0 + if on: + forceon = 1 + if self.ipmi_session.ipmiversion < 2.0: + # ipmi 1.5 made due with just one byte, make best effort + # to imitate indefinite as close as possible + identifydata = [255 * forceon] + else: + identifydata = [0, forceon] + response = await self.raw_command(netfn=0, command=4, data=identifydata) + + async def init_sdr(self): + """Initialize SDR + + Do the appropriate action to have a relevant sensor description + repository for the current management controller + """ + # For now, return current sdr if it exists and still connected + # future, check SDR timestamp for continued relevance + # further future, optionally support a cache directory/file + # to store cached copies for given device id, product id, mfg id, + # sdr timestamp, our data version revision, aux firmware revision, + # and oem defined field + await self.oem_init() + if self._sdr is None: + if hasattr(self._oem, 'init_sdr'): + self._sdr = await self._oem.init_sdr() + else: + self._sdr = sdr.SDR(self, self._sdrcachedir) + await self._sdr.initialize() + return self._sdr + + async def get_event_constants(self): + await self.oem_init() + return await self._oem.get_oem_event_const() + + async def get_event_log(self, clear=False): + """Retrieve the log of events, optionally clearing + + The contents of the SEL are returned as an iterable. Timestamps + are given as local time, ISO 8601 (whether the target has an accurate + clock or not). Timestamps may be omitted for events that cannot be + given a timestamp, leaving only the raw timecode to provide relative + time information. clear set to true will result in the log being + cleared as it is returned. This allows an atomic fetch and clear + behavior so that no log entries will be lost between the fetch and + clear actions. There is no 'clear_event_log' function to encourage + users to create code that is not at risk for losing events. + + :param clear: Whether to remove the SEL entries from the target BMC + """ + await self.oem_init() + evthdlr = await sel.EventHandler.create(await self.init_sdr(), self) + async for logent in evthdlr.fetch_sel(self, clear): + yield logent + + async def decode_pet(self, specifictrap, petdata): + """Decode PET to an event + + In IPMI, the alert format are PET alerts. It is a particular set of + data put into an SNMPv1 trap and sent. It bears no small resemblence + to the SEL entries. This function takes data that would have been + received by an SNMP trap handler, and provides an event decode, similar + to one entry of get_event_log. + + :param specifictrap: The specific trap, as either a bytearray or int + :param petdata: An iterable of the octet data of varbind for + 1.3.6.1.4.1.3183.1.1.1 + :returns: A dict event similar to one iteration of get_event_log + """ + await self.oem_init() + evthdlr = await sel.EventHandler.create(await self.init_sdr(), self) + return await evthdlr.decode_pet(specifictrap, petdata) + + async def get_ikvm_methods(self): + await self.oem_init() + return await self._oem.get_ikvm_methods() + + async def get_ikvm_launchdata(self): + await self.oem_init() + return await self._oem.get_ikvm_launchdata() + + async def get_inventory_descriptions(self): + """Retrieve list of things that could be inventoried + + This permits a caller to examine the available items + without actually causing the inventory data to be gathered. It + returns an iterable of string descriptions + """ + yield "System" + await self.init_sdr() + for fruid in sorted(self._sdr.fru): + yield self._sdr.fru[fruid].fru_name + await self.oem_init() + async for compname in self._oem.get_oem_inventory_descriptions(): + yield compname + + async def get_inventory_of_component(self, component): + """Retrieve inventory of a component + + Retrieve detailed inventory information for only the requested + component. + """ + await self.oem_init() + if component == 'System': + return await self._get_zero_fru() + await self.init_sdr() + for fruid in self._sdr.fru: + if self._sdr.fru[fruid].fru_name == component: + return self._oem.process_fru(fru.FRU( + ipmicmd=self, fruid=fruid, + sdr=self._sdr.fru[fruid]).info, component) + return await self._oem.get_inventory_of_component(component) + + async def _get_zero_fru(self): + # Add some fields returned by get device ID command to FRU 0 + # Also rename them to something more in line with FRU 0 field naming + # standards + device_id = await self._get_device_id() + device_id['Device ID'] = device_id.pop('device_id') + device_id['Device Revision'] = device_id.pop('device_revision') + device_id['Manufacturer ID'] = device_id.pop('manufacturer_id') + device_id['Product ID'] = device_id.pop('product_id') + tfru = fru.FRU(ipmicmd=self, fruid=0) + await tfru.initialize() + zerofru = tfru.info + if zerofru is None: + zerofru = {} + zerofru.update(device_id) + zerofru = await self._oem.process_zero_fru(zerofru) + # If uuid is not returned in OEM processing, + # then it is expected that a manufacturer matches SMBIOS to IPMI + # get system uuid return data. + if 'UUID' not in zerofru: + guiddata = await self.raw_command(netfn=6, command=0x37) + zerofru['UUID'] = util.\ + decode_wireformat_uuid(guiddata['data']) + return zerofru + + async def get_inventory(self): + """Retrieve inventory of system + + Retrieve inventory of the targeted system. This frequently includes + serial numbers, sometimes hardware addresses, sometimes memory modules + This function will retrieve whatever the underlying platform provides + and apply some structure. Iterating over the return yields tuples + of a name for the inventoried item and dictionary of descriptions + or None for items not present. + """ + await self.oem_init() + yield ("System", await self._get_zero_fru()) + await self.init_sdr() + for fruid in sorted(self._sdr.fru): + tfru = fru.FRU(ipmicmd=self, fruid=fruid, + sdr=self._sdr.fru[fruid]) + await tfru.initialize() + fruinf = tfru.info + if fruinf is not None: + fruinf = await self._oem.process_fru(fruinf, + self._sdr.fru[fruid].fru_name) + # check the fruinf again as the oem process may return None + if fruinf: + yield (self._sdr.fru[fruid].fru_name, fruinf) + async for componentpair in self._oem.get_oem_inventory(): + yield componentpair + + async def get_leds(self): + """Get LED status information + + This provides a detailed view of the LEDs of the managed system. + """ + await self.oem_init() + return await self._oem.get_leds() + + async def get_ntp_enabled(self): + await self.oem_init() + return await self._oem.get_ntp_enabled() + + async def set_ntp_enabled(self, enable): + await self.oem_init() + return await self._oem.set_ntp_enabled(enable) + + async def get_ntp_servers(self): + await self.oem_init() + return await self._oem.get_ntp_servers() + + async def set_ntp_server(self, server, index=0): + await self.oem_init() + return await self._oem.set_ntp_server(server, index) + + async def get_health(self): + """Summarize health of managed system + + This provides a summary of the health of the managed system. + It additionally provides an iterable list of reasons for + warning, critical, or failed assessments. + """ + summary = {'badreadings': [], 'health': const.Health.Ok} + fallbackreadings = [] + try: + await self.oem_init() + fallbackreadings = await self._oem.get_health(summary) + async for reading in self.get_sensor_data(): + if reading.health != const.Health.Ok: + summary['health'] |= reading.health + summary['badreadings'].append(reading) + except exc.BypassGenericBehavior: + pass + if not summary['badreadings']: + summary['badreadings'] = fallbackreadings + return summary + + async def get_system_power_watts(self): + await self.oem_init() + return await self._oem.get_system_power_watts(self) + + async def get_inlet_temperature(self): + await self.oem_init() + return await self._oem.get_inlet_temperature(self) + + async def get_average_processor_temperature(self): + await self.oem_init() + return await self._oem.get_average_processor_temperature(self) + + async def get_sensor_reading(self, sensorname): + """Get a sensor reading by name + + Returns a single decoded sensor reading per the name + passed in + + :param sensorname: Name of the desired sensor + :returns: sdr.SensorReading object + """ + await self.init_sdr() + for sensor in self._sdr.get_sensor_numbers(): + if self._sdr.sensors[sensor].name == sensorname: + currsensor = self._sdr.sensors[sensor] + rsp = await self.raw_command(command=0x2d, netfn=4, + rslun=currsensor.sensor_lun, + data=(currsensor.sensor_number,)) + return self._sdr.sensors[sensor].decode_sensor_reading( + self, rsp['data']) + await self.oem_init() + return await self._oem.get_sensor_reading(sensorname) + + async def _fetch_lancfg_param(self, channel, param, prefixlen=False): + """Internal helper for fetching lan cfg parameters + + If the parameter revison != 0x11, bail. Further, if 4 bytes, return + string with ipv4. If 6 bytes, colon delimited hex (mac address). If + one byte, return the int value + """ + fetchcmd = bytearray((channel, param, 0, 0)) + try: + fetched = await self.oldraw_command(0xc, 2, data=fetchcmd) + except exc.IpmiException as ie: + if ie.ipmicode == 0x80: + return None + raise + fetchdata = fetched['data'] + if bytearray(fetchdata)[0] != 17: + return None + if param == 0x14: + vlaninfo = struct.unpack('BB', currpol['data'][2:4]) + if not polidx & 0b1000: + if availpolnum is None: + availpolnum = polnum + continue + if chandest == desiredchandest: + return True + # If chandest did not equal desiredchandest ever, we need to use a slot + if availpolnum is None: + raise Exception("No available alert policy entry") + # 24 = 1 << 4 | 8 + # 1 == set to which this rule belongs + # 8 == 0b1000, in other words, enable this policy, always send to + # indicated destination + await self.raw_command(netfn=4, command=0x12, + data=(9, availpolnum, 24, + desiredchandest, 0)) + + async def get_alert_community(self, channel=None): + """Get the current community string for alerts + + Returns the community string that will be in SNMP traps from this + BMC + + :param channel: The channel to get configuration for, autodetect by + default + :returns: The community string + """ + if channel is None: + channel = await self.get_network_channel() + rsp = await self.raw_command(netfn=0xc, command=2, data=(channel, 16, 0, 0)) + return rsp['data'][1:].partition('\x00')[0] + + async def _supports_standard_ipv6(self): + # Supports the *standard* ipv6 commands for various things + # used to internally steer some commands to standard or OEM + # handler of commands + lanchan = await self.get_network_channel() + if self._ipv6support is None: + rsp = await self.raw_command(netfn=0xc, command=0x2, data=(2, lanchan, + 0x32, 0, 0)) + self._ipv6support = rsp['code'] == 0 + return self._ipv6support + + async def set_alert_destination(self, ip=None, acknowledge_required=None, + acknowledge_timeout=None, retries=None, + destination=0, channel=None): + """Configure one or more parameters of an alert destination + + If any parameter is 'None' (default), that parameter is left unchanged. + Otherwise, all given parameters are set by this command. + + :param ip: IP address of the destination. It is currently expected + that the calling code will handle any name lookup and + present this data as IP address. + :param acknowledge_required: Whether or not the target should expect + an acknowledgement from this alert target. + :param acknowledge_timeout: Time to wait for acknowledgement if enabled + :param retries: How many times to attempt transmit of an alert. + :param destination: Destination index, defaults to 0. + :param channel: The channel to configure the alert on. Defaults to + current + """ + await self.oem_init() + if hasattr(self._oem, 'set_alert_destination'): + await self._oem.set_alert_destination(ip) + return + if channel is None: + channel = await self.get_network_channel() + + if (acknowledge_required is not None + or retries is not None + or acknowledge_timeout is not None): + currtype = await self.raw_command(netfn=0xc, command=2, data=( + channel, 18, destination, 0)) + if currtype['data'][0] != b'\x11': + raise exc.PyghmiException("Unknown parameter format") + currtype = bytearray(currtype['data'][1:]) + if acknowledge_required is not None: + if acknowledge_required: + currtype[1] |= 0b10000000 + else: + currtype[1] &= 0b1111111 + # set PET trap destination + currtype[1] &= 0b1111000 + if acknowledge_timeout is not None: + currtype[2] = acknowledge_timeout + if retries is not None: + currtype[3] = retries + destreq = bytearray((channel, 18)) + destreq.extend(currtype) + await self.raw_command(netfn=0xc, command=1, data=destreq) + if ip is not None: + destdata = bytearray((channel, 19, destination)) + try: + parsedip = socket.inet_pton(socket.AF_INET, ip) + destdata.extend((0, 0)) + destdata.extend(parsedip) + destdata.extend('\x00\x00\x00\x00\x00\x00') + except socket.error: + parsedip = socket.inet_pton(socket.AF_INET6, ip) + destdata.append(0b10000000) + destdata.extend(parsedip) + await self.raw_command(netfn=0xc, command=1, data=destdata) + if not ip == '0.0.0.0': + await self._assure_alert_policy(channel, destination) + + async def get_hostname(self): + """Get the hostname used by the BMC in various contexts + + This can vary somewhat in interpretation, but generally speaking + this should be the name that shows up on UIs and in DHCP requests and + DNS registration requests, as applicable. + + :return: current hostname + """ + await self.oem_init() + try: + return await self._oem.get_hostname() + except exc.UnsupportedFunctionality: + # Use the DCMI MCI field as a fallback, since it's the closest + # thing in the IPMI Spec for this + return await self.get_mci() + + async def get_mci(self): + """Set the management controller identifier. + + Try the OEM command first,if False, then set it per DCMI specification + + :returns: The identifier as a string + """ + await self.oem_init() + identifier = await self._oem.get_oem_identifier() + if identifier: + return identifier + return await self._chunkwise_dcmi_fetch(9) + + async def set_hostname(self, hostname): + """Set the hostname to be used by the BMC in various contexts. + + See get_hostname for details + + :param hostname: The hostname to set + :return: Nothing + """ + await self.oem_init() + try: + return await self._oem.set_hostname(hostname) + except exc.UnsupportedFunctionality: + return await self.set_mci(hostname) + + async def set_mci(self, mci): + """Set the management controller identifier. + + Try the OEM command first, if False, then set it per DCMI specification + """ + await self.oem_init() + if not isinstance(mci, bytes): + mci = mci.encode('utf8') + ret = await self._oem.set_oem_identifier(mci) + if ret: + return + return await self._chunkwise_dcmi_set(0xa, mci + b'\x00') + + async def get_asset_tag(self): + """Get the system asset tag, per DCMI specification + + :returns: The asset tag + """ + await self.oem_init() + if hasattr(self._oem, 'get_asset_tag'): + return await self._oem.get_asset_tag() + return await self._chunkwise_dcmi_fetch(6) + + async def set_asset_tag(self, tag): + """Set the asset tag value + + """ + await self.oem_init() + if hasattr(self._oem, 'set_asset_tag'): + return await self._oem.set_asset_tag(tag) + return await self._chunkwise_dcmi_set(8, tag) + + async def _chunkwise_dcmi_fetch(self, command): + szdata = await self.raw_command( + netfn=0x2c, command=command, data=(0xdc, 0, 0)) + totalsize = bytearray(szdata['data'])[1] + chksize = 0xf + offset = 0 + retstr = b'' + while offset < totalsize: + if (offset + chksize) > totalsize: + chksize = totalsize - offset + chk = await self.raw_command( + netfn=0x2c, command=command, data=(0xdc, offset, chksize)) + retstr += chk['data'][2:] + offset += chksize + if not isinstance(retstr, str): + retstr = retstr.decode('utf-8') + return retstr + + async def _chunkwise_dcmi_set(self, command, data): + chunks = [data[i:i + 15] for i in range(0, len(data), 15)] + offset = 0 + for chunk in chunks: + chunk = bytearray(chunk) + cmddata = bytearray((0xdc, offset, len(chunk))) + cmddata += chunk + # set offset, otherwise the last setting will override + # the previous setting + offset += len(chunk) + await self.raw_command(netfn=0x2c, command=command, data=cmddata) + + async def set_channel_access(self, channel=None, + access_update_mode='non_volatile', + alerting=False, per_msg_auth=False, + user_level_auth=False, access_mode='always', + privilege_update_mode='non_volatile', + privilege_level='administrator'): + """Set channel access + + :param channel: number [1:7] + + :param access_update_mode: + dont_change = don't set or change Channel Access + non_volatile = set non-volatile Channel Access + volatile = set volatile (active) setting of Channel Access + + :param alerting: PEF Alerting Enable/Disable + True = enable PEF Alerting + False = disable PEF Alerting on this channel + (Alert Immediate command can still be used to generate alerts) + + :param per_msg_auth: Per-message Authentication + True = enable + False = disable Per-message Authentication. [Authentication required to + activate any session on this channel, but authentication not + used on subsequent packets for the session.] + + :param user_level_auth: User Level Authentication Enable/Disable. + True = enable User Level Authentication. All User Level commands are + to be authenticated per the Authentication Type that was + negotiated when the session was activated. + False = disable User Level Authentication. Allow User Level commands to + be executed without being authenticated. + If the option to disable User Level Command authentication is + accepted, the BMC will accept packets with Authentication Type + set to None if they contain user level commands. + For outgoing packets, the BMC returns responses with the same + Authentication Type that was used for the request. + + :param access_mode: Access Mode for IPMI messaging + (PEF Alerting is enabled/disabled separately from IPMI messaging) + disabled = disabled for IPMI messaging + pre_boot = pre-boot only channel only available when system is in a + powered down state or in BIOS prior to start of boot. + always = channel always available regardless of system mode. + BIOS typically dedicates the serial connection to the BMC. + shared = same as always available, but BIOS typically leaves the + serial port available for software use. + + :param privilege_update_mode: Channel Privilege Level Limit. + This value sets the maximum privilege level + that can be accepted on the specified channel. + dont_change = don't set or change channel Privilege Level Limit + non_volatile = non-volatile Privilege Level Limit according + volatile = volatile setting of Privilege Level Limit + + :param privilege_level: Channel Privilege Level Limit + * reserved = unused + * callback + * user + * operator + * administrator + * proprietary = used by OEM + """ + if channel is None: + channel = await self.get_network_channel() + data = [] + data.append(channel & 0b00001111) + access_update_modes = { + 'dont_change': 0, + 'non_volatile': 1, + 'volatile': 2, + # 'reserved': 3 + } + b = 0 + b |= (access_update_modes[access_update_mode] << 6) & 0b11000000 + if alerting: + b |= 0b00100000 + if per_msg_auth: + b |= 0b00010000 + if user_level_auth: + b |= 0b00001000 + access_modes = { + 'disabled': 0, + 'pre_boot': 1, + 'always': 2, + 'shared': 3, + } + b |= access_modes[access_mode] & 0b00000111 + data.append(b) + b = 0 + privilege_update_modes = { + 'dont_change': 0, + 'non_volatile': 1, + 'volatile': 2, + # 'reserved': 3 + } + b |= (privilege_update_modes[privilege_update_mode] << 6) & 0b11000000 + privilege_levels = { + 'reserved': 0, + 'callback': 1, + 'user': 2, + 'operator': 3, + 'administrator': 4, + 'proprietary': 5, + # 'no_access': 0x0F, + } + b |= privilege_levels[privilege_level] & 0b00000111 + data.append(b) + response = self.raw_command(netfn=0x06, command=0x40, data=data) + if 'error' in response: + raise Exception(response['error']) + return True + + async def get_channel_access(self, channel=None, read_mode='volatile'): + """Get channel access + + :param channel: number [1:7] + :param read_mode: + non_volatile = get non-volatile Channel Access + volatile = get present volatile (active) setting of Channel Access + + :return: A Python dict with the following keys/values: + { + - alerting: + - per_msg_auth: + - user_level_auth: + - access_mode:{ + 0: 'disabled', + 1: 'pre_boot', + 2: 'always', + 3: 'shared' + } + - privilege_level: { + 1: 'callback', + 2: 'user', + 3: 'operator', + 4: 'administrator', + 5: 'proprietary', + } + } + """ + if channel is None: + channel = await self.get_network_channel() + data = [] + data.append(channel & 0b00001111) + b = 0 + read_modes = { + 'non_volatile': 1, + 'volatile': 2, + } + b |= (read_modes[read_mode] << 6) & 0b11000000 + data.append(b) + + response = await self.raw_command(netfn=0x06, command=0x41, data=data) + if 'error' in response: + raise Exception(response['error']) + + data = response['data'] + if len(data) != 2: + raise Exception('expecting 2 data bytes') + + r = {} + r['alerting'] = data[0] & 0b10000000 > 0 + r['per_msg_auth'] = data[0] & 0b01000000 > 0 + r['user_level_auth'] = data[0] & 0b00100000 > 0 + access_modes = { + 0: 'disabled', + 1: 'pre_boot', + 2: 'always', + 3: 'shared' + } + r['access_mode'] = access_modes[data[0] & 0b00000011] + privilege_levels = { + 0: 'reserved', + 1: 'callback', + 2: 'user', + 3: 'operator', + 4: 'administrator', + 5: 'proprietary', + # 0x0F: 'no_access' + } + r['privilege_level'] = privilege_levels[data[1] & 0b00001111] + return r + + async def get_screenshot(self, outfile): + await self.oem_init() + return await self._oem.get_screenshot(outfile) + + async def get_channel_info(self, channel=None): + """Get channel info + + :param channel: number [1:7] + + :return: + session_support: + no_session: channel is session-less + single: channel is single-session + multi: channel is multi-session + auto: channel is session-based (channel could alternate between + single- and multi-session operation, as can occur with a + serial/modem channel that supports connection mode auto-detect) + """ + if channel is None: + channel = await self.get_network_channel() + data = [] + data.append(channel & 0b00001111) + response = await self.raw_command(netfn=0x06, command=0x42, data=data) + if 'error' in response: + raise Exception(response['error']) + data = response['data'] + if len(data) != 9: + raise Exception('expecting 10 data bytes got: {0}'.format(data)) + r = {} + r['Actual channel'] = data[0] & 0b00000111 + channel_medium_types = { + 0: 'reserved', + 1: 'IPMB', + 2: 'ICMB v1.0', + 3: 'ICMB v0.9', + 4: '802.3 LAN', + 5: 'Asynch. Serial/Modem (RS-232)', + 6: 'Other LAN', + 7: 'PCI SMBus', + 8: 'SMBus v1.0/1.1', + 9: 'SMBus v2.0', + 0x0a: 'reserved for USB 1.x', + 0x0b: 'reserved for USB 2.x', + 0x0c: 'System Interface (KCS, SMIC, or BT)', + # 60h-7Fh: OEM + # all other reserved + } + t = data[1] & 0b01111111 + if t in channel_medium_types: + r['Channel Medium type'] = channel_medium_types[t] + else: + r['Channel Medium type'] = 'OEM {:02X}'.format(t) + r['5-bit Channel IPMI Messaging Protocol Type'] = data[2] & 0b00001111 + session_supports = { + 0: 'no_session', + 1: 'single', + 2: 'multi', + 3: 'auto' + } + r['session_support'] = session_supports[(data[3] & 0b11000000) >> 6] + r['active_session_count'] = data[3] & 0b00111111 + r['Vendor ID'] = [data[4], data[5], data[6]] + r['Auxiliary Channel Info'] = [data[7], data[8]] + return r + + async def set_user_access(self, uid, channel=None, callback=False, + link_auth=True, ipmi_msg=True, privilege_level='user'): + """Set user access + + :param uid: user number [1:16] + + :param channel: number [1:7] + + :param callback: User Restricted to Callback + False = User Privilege Limit is determined by the User Privilege Limit + parameter, below, for both callback and non-callback connections. + True = User Privilege Limit is determined by the User Privilege Limit + parameter for callback connections, but is restricted to Callback + level for non-callback connections. Thus, a user can only initiate + a Callback when they 'call in' to the BMC, but once the callback + connection has been made, the user could potentially establish a + session as an Operator. + + :param link_auth: User Link authentication + enable/disable (used to enable whether this + user's name and password information will be used for link + authentication, e.g. PPP CHAP) for the given channel. Link + authentication itself is a global setting for the channel and is + enabled/disabled via the serial/modem configuration parameters. + + :param ipmi_msg: User IPMI Messaginge: + (used to enable/disable whether + this user's name and password information will be used for IPMI + Messaging. In this case, 'IPMI Messaging' refers to the ability to + execute generic IPMI commands that are not associated with a + particular payload type. For example, if IPMI Messaging is disabled for + a user, but that user is enabled for activatallow_authing the SOL + payload type, then IPMI commands associated with SOL and session + management, such as Get SOL Configuration Parameters and Close Session + are available, but generic IPMI commands such as Get SEL Time are + unavailable.) + + :param privilege_level: + User Privilege Limit. (Determines the maximum privilege level that the + user is allowed to switch to on the specified channel.) + * callback + * user + * operator + * administrator + * proprietary + * no_access + * custom. + """ + await self.oem_init() + if hasattr(self._oem, 'oem_user_access'): + callback, privilege_level = self._oem.oem_user_access( + callback, privilege_level) + + if channel is None: + channel = await self.get_network_channel() + b = 0b10000000 + if callback: + b |= 0b01000000 + if link_auth: + b |= 0b00100000 + if ipmi_msg: + b |= 0b00010000 + b |= channel & 0b00001111 + privilege_levels = { + 'reserved': 0, + 'callback': 1, + 'user': 2, + 'operator': 3, + 'administrator': 4, + 'proprietary': 5, + 'no_access': 0x0F, + } + await self._oem.set_user_access( + uid, channel, callback, link_auth, ipmi_msg, privilege_level) + if privilege_level.startswith('custom.'): + return True # unable to proceed with standard support + data = [b, uid & 0b00111111, + privilege_levels[privilege_level] & 0b00001111, 0] + response = await self.raw_command(netfn=0x06, command=0x43, data=data) + if 'error' in response: + raise Exception(response['error']) + # Set KVM and VMedia Allowed if is administrator + if privilege_level == 'administrator': + await self.set_extended_privilleges(uid) + return True + + async def get_user_access(self, uid, channel=None): + """Get user access + + :param uid: user number [1:16] + :param channel: number [1:7] + + :return: + channel_info: + max_user_count = maximum number of user IDs on this channel + enabled_users = count of User ID slots presently in use + users_with_fixed_names = count of user IDs with fixed names + + access: + callback + link_auth + ipmi_msg + privilege_level: [reserved, callback, user, + operatorm administrator, proprietary, no_access] + """ + # user access available during call-in or callback direct connection + if channel is None: + channel = await self.get_network_channel() + data = [channel, uid] + response = await self.raw_command(netfn=0x06, command=0x44, data=data) + if 'error' in response: + raise Exception(response['error']) + data = response['data'] + if len(data) != 4: + raise Exception('expecting 4 data bytes') + r = {'channel_info': {}, 'access': {}} + r['channel_info']['max_user_count'] = data[0] + r['channel_info']['enabled_users'] = data[1] & 0b00111111 + r['channel_info']['users_with_fixed_names'] = data[2] & 0b00111111 + r['access']['callback'] = (data[3] & 0b01000000) != 0 + r['access']['link_auth'] = (data[3] & 0b00100000) != 0 + r['access']['ipmi_msg'] = (data[3] & 0b00010000) != 0 + await self.oem_init() + oempriv = await self._oem.get_user_privilege_level(uid) + if oempriv: + r['access']['privilege_level'] = oempriv + else: + privilege_levels = { + 0: 'reserved', + 1: 'callback', + 2: 'user', + 3: 'operator', + 4: 'administrator', + 5: 'proprietary', + 0x0F: 'no_access' + } + r['access']['privilege_level'] = privilege_levels[data[3] & 0b00001111] + return r + + async def set_user_name(self, uid, name): + """Set user name + + :param uid: user number [1:16] + :param name: username (limit of 16bytes) + """ + data = [uid] + if not isinstance(name, bytes): + name = name.encode('utf-8') + if len(name) > 16: + raise Exception('name must be less than or = 16 chars') + name = name.ljust(16, b'\x00') + name = bytearray(name) + data.extend(name) + # set timeout to 2s to avoid retry causing enable failure + await self.raw_command(netfn=0x06, command=0x45, data=data, timeout=2) + return True + + async def get_user_name(self, uid, return_none_on_error=True): + """Get user name + + :param uid: user number [1:16] + :param return_none_on_error: return None on error + TODO: investigate return code on error + """ + response = await self.raw_command(netfn=0x06, command=0x46, data=(uid,)) + if 'error' in response: + if return_none_on_error: + return None + raise Exception(response['error']) + name = None + if 'data' in response: + data = response['data'] + if len(data) == 16: + # convert int array to string + n = ''.join(chr(data[i]) for i in range(0, len(data))) + # remove padded \x00 chars + n = n.rstrip("\x00") + if len(n) > 0: + name = n + return name + + async def set_user_password(self, uid, mode='set_password', password=None): + """Set user password and (modes) + + :param uid: id number of user. see: get_names_uid()['name'] + + :param mode: + disable = disable user connections + enable = enable user connections + set_password = set or ensure password + test_password = test password is correct + + :param password: max 16 char string + (optional when mode is [disable or enable]) + + :return: + True on success + when mode = test_password, return False on bad password + """ + mode_mask = { + 'disable': 0, + 'enable': 1, + 'set_password': 2, + 'test_password': 3 + } + data = [uid, mode_mask[mode]] + if password: + if not isinstance(password, bytes): + password = password.encode('utf8') + if 21 > len(password) > 16: + password = password.ljust(20, b'\x00') + data[0] |= 0b10000000 + elif len(password) > 20: + raise Exception('password has limit of 20 chars') + else: + password = password.ljust(16, b'\x00') + data.extend(bytearray(password)) + + await self.oem_init() + data = await self._oem.process_password(password, data) + try: + await self.raw_command(netfn=0x06, command=0x47, data=data) + except exc.IpmiException as ie: + if mode == 'test_password': + return False + elif mode in ('enable', 'disable') and ie.ipmicode == 0xcc: + # Some BMCs see redundant calls to password disable/enable + # as invalid + return True + raise + return True + + async def get_channel_max_user_count(self, channel=None): + """Get max users in channel (helper) + + :param channel: number [1:7] + :return: int -- often 16 + """ + if channel is None: + channel = await self.get_network_channel() + access = await self.get_user_access(channel=channel, uid=1) + return access['channel_info']['max_user_count'] + + async def get_user(self, uid, channel=None): + """Get user (helper) + + :param uid: user number [1:16] + :param channel: number [1:7] + + :return: + name: (str) + uid: (int) + channel: (int) + access: + callback (bool) + link_auth (bool) + ipmi_msg (bool) + privilege_level: (str)[callback, user, operatorm administrator, + proprietary, no_access] + expiration: + None for 'unknown', 0 for no expiry, days to expire otherwise. + """ + if channel is None: + channel = await self.get_network_channel() + name = await self.get_user_name(uid) + access = await self.get_user_access(uid, channel) + await self.oem_init() + expiration = await self._oem.get_user_expiration(uid) + data = {'name': name, 'uid': uid, 'channel': channel, + 'access': access['access'], 'expiration': expiration} + return data + + async def get_name_uids(self, name, channel=None): + """get list of users (helper) + + :param channel: number [1:7] + + :return: list of users + """ + if channel is None: + channel = await self.get_network_channel() + uid_list = [] + max_ids = await self.get_channel_max_user_count(channel) + for uid in range(1, max_ids): + if name == await self.get_user_name(uid=uid): + uid_list.append(uid) + return uid_list + + async def get_users(self, channel=None): + """get list of users and channel access information (helper) + + :param channel: number [1:7] + + :return: + name: (str) + uid: (int) + channel: (int) + access: + callback (bool) + link_auth (bool) + ipmi_msg (bool) + privilege_level: (str)[callback, user, operatorm administrator, + proprietary, no_access] + """ + await self.oem_init() + if channel is None: + channel = await self.get_network_channel() + names = {} + max_ids = await self.get_channel_max_user_count(channel) + for uid in range(1, max_ids + 1): + name = await self.get_user_name(uid=uid) + if await self._oem.is_valid(name): + names[uid] = await self.get_user(uid=uid, channel=channel) + return names + + async def create_user(self, uid, name, password, channel=None, callback=False, + link_auth=True, ipmi_msg=True, + privilege_level='user'): + """create/ensure a user is created with provided settings (helper) + + :param privilege_level: + User Privilege Limit. (Determines the maximum privilege level that + the user is allowed to switch to on the specified channel.) + * callback + * user + * operator + * administrator + * proprietary + * no_access + """ + # current user might be trying to update.. dont disable + # set_user_password(uid, password, mode='disable') + if channel is None: + channel = await self.get_network_channel() + await self.set_user_name(uid, name) + await self.set_user_password(uid, password=password) + await self.set_user_password(uid, mode='enable', password=password) + await self.set_user_access(uid, channel, callback=callback, + link_auth=link_auth, ipmi_msg=ipmi_msg, + privilege_level=privilege_level) + return True + + async def user_delete(self, uid, channel=None): + """Delete user (helper) + + Note that in IPMI, user 'deletion' isn't a concept. This function + will make a best effort to provide the expected result (e.g. + web interfaces skipping names and ipmitool skipping as well. + + :param uid: user number [1:16] + :param channel: number [1:7] + """ + # TODO(jjohnson2): Provide OEM extensibility to cover user deletion + await self.oem_init() + if hasattr(self._oem, 'user_delete'): + try: + await self._oem.user_delete(uid, channel) + except exc.BypassGenericBehavior: + return + if hasattr(self._oem, 'user_delete_privilege_level'): + privilege_level = await self._oem.user_delete_privilege_level() + else: + privilege_level = 'no_access' + + if channel is None: + channel = await self.get_network_channel() + await self.set_user_password(uid, mode='disable', password=None) + # TODO(steveweber) perhaps should set user access on all channels + # so new users dont get extra access + await self.set_user_access(uid, channel=channel, callback=False, + link_auth=False, ipmi_msg=False, + privilege_level=privilege_level) + try: + # First try to set name to all \x00 explicitly + # Fix bug-174389, don't try to send \xff command, + # will cause IMM1 can't login + await self.set_user_name(uid, '') + except exc.IpmiException: + raise + return True + + async def disable_user(self, uid, mode): + """Disable User + + Just disable the User. + This will not disable the password or revoke privileges. + + :param uid: user number [1:16] + :param mode: + disable = disable user connections + enable = enable user connections + """ + await self.set_user_password(uid, mode) + return True + + async def update_user(self, user): + """Update User + + Update user attributes, include name, password, access + + :param user: user attributes + """ + if 'username' in user: + await self.set_user_name(uid=user['uid'], name=user['username']) + + privilege_level = None + if 'privilege_level' in user: + privilege_level = user['privilege_level'] + + if privilege_level and privilege_level == 'no_access': + return await self.upate_user_no_access(user, privilege_level) + else: + return await self.update_user_default(user, privilege_level) + + async def upate_user_no_access(self, user, privilege_level): + # if set to no_access, modify password first + if 'password' in user: + await self.set_user_password(uid=user['uid'], password=user['password']) + await self.set_user_access(uid=user['uid'], privilege_level=privilege_level) + + return True + + async def update_user_default(self, user, privilege_level): + if privilege_level: + await self.set_user_access(uid=user['uid'], + privilege_level=privilege_level) + if 'password' in user: + await self.set_user_password(uid=user['uid'], + password=user['password']) + await self.set_user_password(uid=user['uid'], mode='enable', + password=user['password']) + if 'enabled' in user: + if user['enabled'] == 'yes': + mode = 'enable' + else: + mode = 'disable' + await self.disable_user(user['uid'], mode) + + return True + + async def get_firmware(self, components=(), category=None): + """Retrieve OEM Firmware information""" + + await self.oem_init() + mcinfo = await self.raw_command(netfn=6, command=1) + major, minor = struct.unpack('BB', mcinfo['data'][2:4]) + bmcver = '{0}.{1}'.format(major, hex(minor)[2:]) + async for x in self._oem.get_oem_firmware(bmcver, components, category): + yield x + + async def get_capping_enabled(self): + """Get PSU based power capping status + + :return: True if enabled and False if disabled + """ + await self.oem_init() + return await self._oem.get_oem_capping_enabled() + + async def set_capping_enabled(self, enable): + """Set PSU based power capping + + :param enable: True for enable and False for disable + """ + await self.oem_init() + return await self._oem.set_oem_capping_enabled(enable) + + async def set_server_capping(self, value): + await self.oem_init() + await self._oem.set_oem_server_capping(value) + + async def get_server_capping(self): + await self.oem_init() + return await self._oem.get_oem_server_capping() + + async def get_remote_kvm_available(self): + """Get remote KVM availability""" + + await self.oem_init() + return await self._oem.get_oem_remote_kvm_available() + + async def get_domain_name(self): + """Get Domain name""" + + await self.oem_init() + return await self._oem.get_oem_domain_name() + + async def set_domain_name(self, name): + """Set Domain name + + :param name: domain name to be set + """ + await self.oem_init() + await self._oem.set_oem_domain_name(name) + + async def get_graphical_console(self): + """Get graphical console launcher""" + await self.oem_init() + return await self._oem.get_graphical_console() + + async def get_update_status(self): + await self.oem_init() + return await self._oem.get_update_status() + + async def update_firmware(self, filename, data=None, progress=None, bank=None): + """Send file to BMC to perform firmware update + + :param filename: The filename to upload to the target BMC + :param data: The payload of the firmware. Default is to read from + specified filename. + :param progress: A callback that will be given a dict describing + update process. Provide if + :param bank: Indicate a target 'bank' of firmware if supported + """ + + await self.oem_init() + if progress is None: + progress = lambda x: True + return await self._oem.update_firmware(filename, data, progress, bank) + + async def attach_remote_media(self, url, username=None, password=None): + """Attach remote media by url + + Given a url, attach remote media (cd/usb image) to the target system. + + :param url: URL to indicate where to find image (protocol support + varies by BMC) + :param username: Username for endpoint to use when accessing the URL. + If applicable, 'domain' would be indicated by '@' or + '\' syntax. + :param password: Password for endpoint to use when accessing the URL. + """ + await self.oem_init() + return await self._oem.attach_remote_media(url, username, password) + + async def detach_remote_media(self): + await self.oem_init() + return await self._oem.detach_remote_media() + + async def upload_media(self, filename, progress=None, data=None): + """Upload a file to be hosted on the target BMC + + This will upload the specified data to + the BMC so that it will make it available to the system as an emulated + USB device. + + :param filename: The filename to use, the basename of the parameter + will be given to the bmc. + :param progress: Optional callback for progress updates + """ + await self.oem_init() + return await self._oem.upload_media(filename, progress, data) + + async def list_media(self): + """List attached remote media + + :returns: An iterable list of attached media + """ + await self.oem_init() + async for m in self._oem.list_media(): + yield m + + async def get_licenses(self): + await self.oem_init() + async for x in self._oem.get_licenses(): + yield x + + async def delete_license(self, name): + await self.oem_init() + return await self._oem.delete_license(name) + + async def save_licenses(self, directory): + if os.path.exists(directory) and not os.path.isdir(directory): + raise exc.InvalidParameterValue( + 'Not allowed to overwrite existing file: {0}'.format( + directory)) + await self.oem_init() + async for lic in self._oem.save_licenses(directory): + yield lic + + async def apply_license(self, filename, progress=None, data=None): + await self.oem_init() + async for x in self._oem.apply_license(filename, progress, data): + yield x + + async def set_extended_privilleges(self, uid): + """Set user extended privillege as 'KVM & VMedia Allowed' + + """ + await self.oem_init() + return await self._oem.set_oem_extended_privilleges(uid) \ No newline at end of file diff --git a/confluent_server/aiohmi/ipmi/console.py b/confluent_server/aiohmi/ipmi/console.py new file mode 100644 index 00000000..6479b938 --- /dev/null +++ b/confluent_server/aiohmi/ipmi/console.py @@ -0,0 +1,551 @@ +# Copyright 2014 IBM Corporation +# Copyright 2015-2019 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 low layer message framing portion of IPMI""" + +import struct +import threading + +import aiohmi.exceptions as exc +from aiohmi.ipmi.private import constants +from aiohmi.ipmi.private import session +from aiohmi.ipmi.private.util import _monotonic_time + + +class Console(object): + """IPMI SOL class. + + This object represents an SOL channel, multiplexing SOL data with + commands issued by ipmi.command. + + :param bmc: hostname or ip address of BMC + :param userid: username to use to connect + :param password: password to connect to the BMC + :param iohandler: Either a function to call with bytes, a filehandle to + use for input and output, or a tuple of (input, output) + handles + :param force: Set to True to force on or False to force off + :param kg: optional parameter for BMCs configured to require it + """ + + # TODO(jbjohnso): still need an exit and a data callin function + def __init__(self, bmc, userid, password, + iohandler, port=623, + force=False, kg=None): + self.outputlock = threading.RLock() + self.keepaliveid = None + self.connected = False + 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 = False + self.force_session = force + self.port = port + self.ipmi_session = None + self.callgotsession = None + self.bmc = bmc + self.userid = userid + self.password = password + self.port = port + self.kg = kg + self.broken = False + + async def connect(self): + bmc = self.bmc + userid = self.userid + password = self.password + port = self.port + kg = self.kg + self.ipmi_session = await session.Session( + bmc=bmc, userid=userid, password=password, port=port, kg=kg) + # induce one iteration of the loop, now that we would be + # prepared for it in theory + await self._got_session({}) + + + async def _got_session(self, response): + """Private function to navigate SOL payload activation""" + if 'error' in response: + await self._print_error(response['error']) + return + if not self.ipmi_session: + self.callgotsession = response + return + # Send activate sol payload directive + # netfn= 6 (application) + # command = 0x48 (activate payload) + # data = (1, sol payload type + # 1, first instance + # 0b11000000, -encrypt, authenticate, + # disable serial/modem alerts, CTS fine + # 0, 0, 0 reserved + response = await self.ipmi_session.raw_command(netfn=0x6, command=0x48, + data=(1, 1, 192, 0, 0, 0)) + # given that these are specific to the command, + # it's probably best if one can grep the error + # here instead of in constants + sol_activate_codes = { + 0x81: 'SOL is disabled', + 0x82: 'Maximum SOL session count reached', + 0x83: 'Cannot activate payload with encryption', + 0x84: 'Cannot activate payload without encryption', + } + if 'code' in response and response['code']: + if response['code'] in constants.ipmi_completion_codes: + await self._print_error( + constants.ipmi_completion_codes[response['code']]) + return + elif response['code'] == 0x80: + if self.force_session and not self.retriedpayload: + self.retriedpayload = 1 + sessrsp = await self.ipmi_session.raw_command( + netfn=0x6, + command=0x49, + data=(1, 1, 0, 0, 0, 0)) + await self._got_session(sessrsp) + return + else: + await self._print_error('SOL Session active for another client') + return + elif response['code'] in sol_activate_codes: + await self._print_error(sol_activate_codes[response['code']]) + return + else: + await self._print_error( + 'SOL encountered Unrecognized error code %d' % + response['code']) + return + if 'error' in response: + self._print_error(response['error']) + return + self.activated = True + # data[0:3] is reserved except for the test mode, which we don't use + data = response['data'] + self.maxoutcount = (data[5] << 8) + data[4] + # BMC tells us this is the maximum allowed size + # data[6:7] is the promise of how small packets are going to be, but we + # don't have any reason to worry about it + # some BMCs disagree on the endianness, so do both + valid_ports = (self.port, struct.unpack( + 'H', self.port))[0]) + if (data[8] + (data[9] << 8)) not in valid_ports: + # TODO(jbjohnso): support atypical SOL port number + raise NotImplementedError("Non-standard SOL Port Number") + # ignore data[10:11] for now, the vlan detail, shouldn't matter to this + # code anyway... + # NOTE(jbjohnso): + # We will use a special purpose keepalive + if self.ipmi_session.sol_handler is not None: + # If there is erroneously another SOL handler already, notify + # it of newly established session + await self.ipmi_session.sol_handler({'error': 'Session Disconnected'}) + self.keepaliveid = self.ipmi_session.register_keepalive( + cmd={'netfn': 6, 'command': 0x4b, 'data': (1, 1)}, + callback=self._got_payload_instance_info) + self.ipmi_session.sol_handler = self._got_sol_payload + self.connected = True + # self._sendpendingoutput() checks len(self._sendpendingoutput) + await self._sendpendingoutput() + + async def _got_payload_instance_info(self, response): + if 'error' in response: + self.activated = False + await self._print_error(response['error']) + return + currowner = struct.unpack( + " self.maxoutcount: + chunk = self.pendingoutput[0][:self.maxoutcount] + self.pendingoutput[0] = self.pendingoutput[0][ + self.maxoutcount:] + else: + chunk = self.pendingoutput[0] + del self.pendingoutput[0] + await self._sendoutput(chunk, sendbreak=dobreak) + + async def _sendoutput(self, output, sendbreak=False): + self.myseq += 1 + self.myseq &= 0xf + if self.myseq == 0: + self.myseq = 1 + # currently we don't try to combine ack with outgoing data + # so we use 0 for ack sequence number and accepted character + # count + breakbyte = 0 + if sendbreak: + breakbyte = 0b10000 + try: + payload = bytearray((self.myseq, 0, 0, breakbyte)) + output + except TypeError: # bytearray hits unicode... + payload = bytearray((self.myseq, 0, 0, breakbyte + )) + output.encode('utf8') + self.lasttextsize = len(output) + needskeepalive = False + if self.lasttextsize == 0: + needskeepalive = True + self.awaitingack = True + self.lastpayload = payload + await self.send_payload(payload, retry=False, needskeepalive=needskeepalive) + retries = 5 + while retries and self.awaitingack: + expiry = _monotonic_time() + 5.5 - retries + while self.awaitingack and _monotonic_time() < expiry: + await self.wait_for_rsp(0.5) + if self.awaitingack: + await self.send_payload(payload, retry=False, + needskeepalive=needskeepalive) + retries -= 1 + if not retries: + await self._print_error('Connection lost') + + async 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) + if self.ipmi_session is None or not self.ipmi_session.logged: + await self._print_error('Session no longer connected') + raise exc.IpmiException('Session no longer connected') + await self.ipmi_session.send_payload(payload, + payload_type=payload_type, + retry=retry, + needskeepalive=needskeepalive) + + async def _print_info(self, info): + await self._print_data({'info': info}) + + async def _print_error(self, error): + self.broken = True + if self.ipmi_session: + self.ipmi_session.unregister_keepalive(self.keepaliveid) + if (self.ipmi_session.sol_handler + and self.ipmi_session.sol_handler.__self__ is self): + self.ipmi_session.sol_handler = None + self.ipmi_session = None + if type(error) == dict: + await self._print_data(error) + else: + await self._print_data({'error': error}) + + async def _print_data(self, data): + """Convey received data back to caller in the format of their choice. + + Caller may elect to provide this class filehandle(s) or else give a + callback function that this class will use to convey data back to + caller. + """ + await self.out_handler(data) + + async 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 + await self._print_error(payload) + return + newseq = payload[0] & 0b1111 + ackseq = payload[1] & 0b1111 + ackcount = payload[2] + nacked = payload[3] & 0b1000000 + poweredoff = payload[3] & 0b100000 + deactivated = payload[3] & 0b10000 + breakdetected = payload[3] & 0b100 + # 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 + 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 = bytes(payload[4:]) + if newseq == self.remseq: # it is a retry, but could have new data + if remdatalen > self.lastsize: + remdata = bytes(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 + if remdata: # Do not subject callers to empty data + await self._print_data(remdata) + ackpayload = bytearray((0, self.remseq, remdatalen, 0)) + # 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: + await self.send_payload(ackpayload, retry=False) + except exc.IpmiException: + # if the session is broken, then close the SOL session + self.close() + 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: + await self._print_info("Remote system is powered down") + if deactivated: + self.activated = False + await 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:] + with self.outputlock: + 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() checks len(self._sendpendingoutput) + await 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 + await self.send_payload(payload=self.lastpayload, retry=False) + + def main_loop(self): + """Process all events until no more sessions exist. + + If a caller is a simple little utility, provide a function to + eternally run the event loop. More complicated usage would be expected + to provide their own event loop behavior, though this could be used + within the greenthread implementation of caller's choice if desired. + """ + # wait_for_rsp promises to return a false value when no sessions are + # alive anymore + # TODO(jbjohnso): wait_for_rsp is not returning a true value for our + # 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.outputlock = threading.RLock() + 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) + + async 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 + await 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 = bytes(payload[4:]) + if newseq == self.remseq: # it is a retry, but could have new data + if remdatalen > self.lastsize: + remdata = bytes(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 = bytearray((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 + await 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:] + with self.outputlock: + 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() checks len(self._sendpendingoutput) + 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/confluent_server/aiohmi/ipmi/events.py b/confluent_server/aiohmi/ipmi/events.py new file mode 100644 index 00000000..c2c4b1ca --- /dev/null +++ b/confluent_server/aiohmi/ipmi/events.py @@ -0,0 +1,589 @@ +# Copyright 2016 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. + +import struct +import time + +import aiohmi.constants as pygconst +import aiohmi.exceptions as pygexc + +try: + range = xrange +except NameError: + pass +try: + buffer +except NameError: + buffer = memoryview + + +psucfg_errors = { + 0: 'Vendor mismatch', + 1: 'Revision mismatch', + 2: 'Processor missing', # e.g. pluggable CPU VRMs... + 3: 'Insufficient power', + 4: 'Voltage mismatch', +} + +firmware_progress = { + 0: 'Unspecified', + 1: 'Memory initialization', + 2: 'Disk initialization', + 3: 'Non-primary Processor initialization', + 4: 'User authentication', + 5: 'In setup', + 6: 'USB initialization', + 7: 'PCI initialization', + 8: 'Option ROM initialization', + 9: 'Video initialization', + 0xa: 'Cache initialization', + 0xb: 'SMBus initialization', + 0xc: 'Keyboard initialization', + 0xd: 'Embedded controller initialization', + 0xe: 'Docking station attachment', + 0xf: 'Docking station enabled', + 0x10: 'Docking station ejection', + 0x11: 'Docking station disabled', + 0x12: 'Waking OS', + 0x13: 'Starting OS boot', + 0x14: 'Baseboard initialization', + 0x16: 'Floppy initialization', + 0x17: 'Keyboard test', + 0x18: 'Pointing device test', + 0x19: 'Primary processor initialization', +} + +firmware_errors = { + 0: 'Unspecified', + 1: 'No memory installed', + 2: 'All memory failed', + 3: 'Unrecoverable disk failure', + 4: 'Unrecoverable board failure', + 5: 'Unrecoverable diskette failure', + 6: 'Unrecoverable storage controller failure', + 7: 'Unrecoverable keyboard failure', # Keyboard error, press + # any key to continue.. + 8: 'Removable boot media not found', + 9: 'Video adapter failure', + 0xa: 'No video device', + 0xb: 'Firmware corruption detected', + 0xc: 'CPU voltage mismatch', + 0xd: 'CPU speed mismatch', +} + +auxlog_actions = { + 0: 'entry added', + 1: 'entry added (could not map to standard)', + 2: 'entry added with corresponding standard events', + 3: 'log cleared', + 4: 'log disabled', + 5: 'log enabled', +} + +restart_causes = { + 0: 'Unknown', + 1: 'Remote request', + 2: 'Reset button', + 3: 'Power button', + 4: 'Watchdog', + 5: 'OEM', + 6: 'Power restored', + 7: 'Power restored', + 8: 'Reset due to event', + 9: 'Cycle due to event', + 0xa: 'OS reset', + 0xb: 'Timer wake', +} + +slot_types = { + 0: 'PCI', + 1: 'Drive Array', + 2: 'External connector', + 3: 'Docking', + 4: 'Other', + 5: 'Entity ID', + 6: 'AdvancedTCA', + 7: 'Memory', + 8: 'Fan', + 9: 'PCIe', + 10: 'SCSI', + 11: 'SATA/SAS', + 12: 'USB', +} + +power_states = { + 0: 'S0', + 1: 'S1', + 2: 'S2', + 3: 'S3', + 4: 'S4', + 5: 'S5', + 6: 'S4 or S5', + 7: 'G3', + 8: 'S1, S2, or S3', + 9: 'G1', + 0xa: 'S5', + 0xb: 'on', + 0xc: 'off', +} + +watchdog_boot_phases = { + 1: 'Firmware', + 2: 'Firmware', + 3: 'OS Load', + 4: 'OS', + 5: 'OEM', +} + +version_changes = { + 1: 'Device ID', + 2: 'Management controller firmware', + 3: 'Management controller revision', + 4: 'Management conroller manufacturer', + 5: 'IPMI version', + 6: 'Management controller firmware', + 7: 'Management controller boot block', + 8: 'Management controller firmware', + 9: 'System Firmware (UEFI/BIOS)', + 0xa: 'SMBIOS', + 0xb: 'OS', + 0xc: 'OS Loader', + 0xd: 'Diagnostics', + 0xe: 'Management agent', + 0xf: 'Management application', + 0x10: 'Management middleware', + 0x11: 'FPGA', + 0x12: 'FRU', + 0x13: 'FRU', + 0x14: 'Equivalent FRU', + 0x15: 'Updated FRU', + 0x16: 'Older FRU', + 0x17: 'Hardware (switch/jumper)', +} + +fru_states = { + 0: 'Normal', + 1: 'Externally requested', + 2: 'Latch', + 3: 'Hot swap', + 4: 'Internal action', + 5: 'Lost communication', + 6: 'Lost communication', + 7: 'Unexpected removal', + 8: 'Operator', + 9: 'Unable to compute IPMB address', + 0xa: 'Unexpected deactivation', +} + + +def decode_eventdata(sensor_type, offset, eventdata, event_consts, sdr): + """Decode extra event data from an alert or log + + Provide a textual summary of eventdata per descriptions in + Table 42-3 of the specification. This is for sensor specific + offset events only. + + :param sensor_type: The sensor type number from the event + :param offset: Sensor specific offset + :param eventdata: The three bytes from the log or alert + :param event_consts: event definition including severity. + :param sdr: The sdr locator entry to help clarify how to parse data + """ + if sensor_type == 5 and offset == 4: # link loss, indicates which port + return 'Port {0}'.format(eventdata[1]) + elif sensor_type == 8 and offset == 6: # PSU cfg error + errtype = eventdata[2] & 0b1111 + return psucfg_errors.get(errtype, 'Unknown') + elif sensor_type == 0xC6: + return 'PSU Redundancy' + elif sensor_type == 0xc and offset == 8: # Memory spare + return 'Module {0}'.format(eventdata[2]) + elif sensor_type == 0xf: + if offset == 0: # firmware error + return firmware_errors.get(eventdata[1], 'Unknown') + elif offset in (1, 2): + return firmware_progress.get(eventdata[1], 'Unknown') + elif sensor_type == 0x10: + if offset == 0: # Correctable error logging on a specific memory part + return 'Module {0}'.format(eventdata[1]) + elif offset == 1: + return 'Reading type {0:02X}h, offset {1:02X}h'.format( + eventdata[1], eventdata[2] & 0b1111) + elif offset == 5: + return '{0}%'.format(eventdata[2]) + elif offset == 6: + return 'Processor {0}'.format(eventdata[1]) + elif sensor_type == 0x12: + if offset == 3: + action = (eventdata[1] & 0b1111000) >> 4 + return auxlog_actions.get(action, 'Unknown') + elif offset == 4: + sysactions = [] + if eventdata[1] & 0b1 << 5: + sysactions.append('NMI') + if eventdata[1] & 0b1 << 4: + sysactions.append('OEM action') + if eventdata[1] & 0b1 << 3: + sysactions.append('Power Cycle') + if eventdata[1] & 0b1 << 2: + sysactions.append('Reset') + if eventdata[1] & 0b1 << 1: + sysactions.append('Power Down') + if eventdata[1] & 0b1: + sysactions.append('Alert') + return ','.join(sysactions) + elif offset == 5: # Clock change event, either before or after + if eventdata[1] & 0b10000000: + return 'After' + else: + return 'Before' + elif sensor_type == 0x19 and offset == 0: + return 'Requested {0} while {1}'.format(eventdata[1], eventdata[2]) + elif sensor_type == 0x1d and offset == 7: + return restart_causes.get(eventdata[1], 'Unknown') + elif sensor_type == 0x21: + return '{0} {1}'.format(slot_types.get(eventdata[1], 'Unknown'), + eventdata[2]) + + elif sensor_type == 0x23: + phase = eventdata[1] & 0b1111 + return watchdog_boot_phases.get(phase, 'Unknown') + elif sensor_type == 0x28: + if offset == 4: + return 'Sensor {0}'.format(eventdata[1]) + elif offset == 5: + islogical = (eventdata[1] & 0b10000000) + if islogical: + if eventdata[2] in sdr.fru: + return sdr.fru[eventdata[2]].fru_name + else: + return 'FRU {0}'.format(eventdata[2]) + elif sensor_type == 0x2a and offset == 3: + return 'User {0}'.format(eventdata[1]) + elif sensor_type == 0x2b: + return version_changes.get(eventdata[1], 'Unknown') + elif sensor_type == 0x2c: + cause = (eventdata[1] & 0b11110000) >> 4 + cause = fru_states.get(cause, 'Unknown') + oldstate = eventdata[1] & 0b1111 + if oldstate != offset: + try: + cause += '(change from {0})'.format( + event_consts.sensor_type_offsets[0x2c][oldstate]['desc']) + except KeyError: + pass + + +async def _fix_sel_time(records, ipmicmd): + timefetched = False + rsp = None + while not timefetched: + try: + rsp = await ipmicmd.raw_command(netfn=0xa, command=0x48) + timefetched = True + except pygexc.IpmiException as pi: + if pi.ipmicode == 0x81: + continue + raise + # The specification declares an epoch and all that, but we really don't + # care. We instead just focus on differences from the 'present' + nowtime = struct.unpack_from(' lasttimestamp): + # if the timestamp did something impossible, declare the rest + # of history not meaningfully correctable + correctionenabled = False + newtimestamp = 0 + continue + newtimestamp = record['timecode'] + trimindexes.append(index) + elif ('event' in record and record['event'] == 'Clock time change' + and record['event_data'] == 'Before'): + if not correctionenabled: + continue + if newtimestamp: + if record['timecode'] < 0x20000000: + correctearly = True + nowtime = correctednowtime + # we want time that occurred before this point to get the delta + # added to it to catch up + correctednowtime += newtimestamp - record['timecode'] + newtimestamp = 0 + trimindexes.append(index) + else: + # clean up after potentially broken time sync pairs + newtimestamp = 0 + if record['timecode'] < 0x20000000: # uptime timestamp + if not correctearly or not correctionenabled: + correctednowtime = nowtime + continue + if (lasttimestamp is not None + and record['timecode'] > lasttimestamp): + # Time has gone backwards in pre-init, no hope for + # accurate time + correctearly = False + correctionenabled = False + correctednowtime = nowtime + continue + inpreinit = True + lasttimestamp = record['timecode'] + age = correctednowtime - record['timecode'] + record['timestamp'] = time.strftime( + '%Y-%m-%dT%H:%M:%S', time.localtime(time.time() - age)) + else: + # We are in 'normal' time, assume we cannot go to + # pre-init time and do corrections unless time sync events + # guide us in safely + if (lasttimestamp is not None + and record['timecode'] > lasttimestamp): + # Time has gone backwards, without a clock sync + # give up any attempt to correct from this point back... + correctionenabled = False + if inpreinit: + inpreinit = False + # We were in pre-init, now in real time, reset the + # time correction factor to the last stored + # 'wall clock' correction + correctednowtime = nowtime + correctearly = False + lasttimestamp = record['timecode'] + if not correctionenabled or correctednowtime < 0x20000000: + # We can't correct time when the correction factor is + # rooted in a pre-init timestamp, just convert + record['timestamp'] = time.strftime( + '%Y-%m-%dT%H:%M:%S', time.localtime( + record['timecode'])) + else: + age = correctednowtime - record['timecode'] + record['timestamp'] = time.strftime( + '%Y-%m-%dT%H:%M:%S', time.localtime( + time.time() - age)) + for index in trimindexes: + del records[index] + + +class EventHandler(object): + """IPMI Event Processor + + This class provides facilities for processing alerts and event log + data. This can be used to aid in pulling historical event data + from a BMC or as part of a trap handler to translate the traps into + manageable data. + + :param sdr: An SDR object (per aiohmi.ipmi.sdr) matching the target BMC SDR + :param ipmicmd: An ipmi command object to fetch data live + """ + @classmethod + async def create(cls, sdr, ipmicmd): + self = cls() + self._sdr = sdr + self._ipmicmd = ipmicmd + self.event_consts = await ipmicmd.get_event_constants() + return self + + def _populate_event(self, deassertion, event, event_data, event_type, + sensor_type, sensorid): + event['component_id'] = sensorid + try: + event['component'] = self._sdr.sensors[sensorid].name + except KeyError: + if sensorid == 0: + event['component'] = None + else: + event['component'] = 'Sensor {0}'.format(sensorid) + event['deassertion'] = deassertion + event['event_data_bytes'] = event_data + byte2type = (event_data[0] & 0b11000000) >> 6 + byte3type = (event_data[0] & 0b110000) >> 4 + if byte2type == 1: + event['triggered_value'] = event_data[1] + evtoffset = event_data[0] & 0b1111 + event['event_type_byte'] = event_type + if event_type <= 0xc: + event['component_type_id'] = sensor_type + event['event_id'] = '{0}.{1}'.format(event_type, evtoffset) + # use generic offset decode for event description + event['component_type'] = self.event_consts.sensor_type_codes.get( + sensor_type, '') + evreading = self.event_consts.generic_type_offsets.get( + event_type, {}).get(evtoffset, {}) + if event['deassertion']: + event['event'] = evreading.get('deassertion_desc', '') + event['severity'] = evreading.get( + 'deassertion_severity', pygconst.Health.Ok) + else: + event['event'] = evreading.get('desc', '') + event['severity'] = evreading.get( + 'severity', pygconst.Health.Ok) + elif event_type == 0x6f: + event['component_type_id'] = sensor_type + event['event_id'] = '{0}.{1}'.format(event_type, evtoffset) + event['component_type'] = self.event_consts.sensor_type_codes.get( + sensor_type, '') + evreading = self.event_consts.sensor_type_offsets.get( + sensor_type, {}).get(evtoffset, {}) + if event['deassertion']: + event['event'] = evreading.get('deassertion_desc', '') + event['severity'] = evreading.get( + 'deassertion_severity', pygconst.Health.Ok) + else: + event['event'] = evreading.get('desc', '') + event['severity'] = evreading.get( + 'severity', pygconst.Health.Ok) + if event_type == 1: # threshold + if byte3type == 1: + event['threshold_value'] = event_data[2] + if 3 in (byte2type, byte3type) or event_type == 0x6f: + # sensor specific decode, see sdr module... + # 2 - 0xc: generic discrete, 0x6f, sensor specific + additionaldata = decode_eventdata( + sensor_type, evtoffset, event_data, self.event_consts, + self._sdr) + if additionaldata: + event['event_data'] = additionaldata + + async def decode_pet(self, specifictrap, petdata): + if isinstance(specifictrap, int): + specifictrap = struct.unpack('4B', struct.pack('>I', specifictrap)) + if len(specifictrap) != 4: + raise pygexc.InvalidParameterValue( + 'specifictrap should be integer number or 4 byte array') + specifictrap = bytearray(specifictrap) + sensor_type = specifictrap[1] + event_type = specifictrap[2] + # Event Offset is in first event data byte, so no need to fetch it here + # evtoffset = specifictrap[3] & 0b1111 + deassertion = (specifictrap[3] & 0b10000000) == 0b10000000 + # alertseverity = petdata[26] + sensorid = '{0}.0'.format(petdata[28]) + event_data = petdata[31:34] + event = {} + seqnum = struct.unpack_from('>H', buffer(petdata[16:18]))[0] + ltimestamp = struct.unpack_from('>I', buffer(petdata[18:22]))[0] + petack = bytearray(struct.pack('= 0xe0: + # In this class of OEM message, all bytes are OEM, interpretation + # is wholly left up to the OEM layer, using the OEM ID of the BMC + event['oemdata'] = selentry[3:] + await self._ipmicmd._oem.process_event(event, self._ipmicmd, selentry) + if 'event_type_byte' in event: + del event['event_type_byte'] + if 'event_data_bytes' in event: + del event['event_data_bytes'] + return event + + async def _fetch_entries(self, ipmicmd, startat, targetlist, rsvid=0): + curr = startat + endat = curr + while curr != 0xffff: + endat = curr + reqdata = bytearray(struct.pack(' 0: + currchunk = inputdata[:3] + del inputdata[:3] + currchar = currchunk[0] & 0b111111 + result += chr(0x20 + currchar) + currchar = (currchunk[0] & 0b11000000) >> 6 + currchar |= (currchunk[1] & 0b1111) << 2 + result += chr(0x20 + currchar) + currchar = (currchunk[1] & 0b11110000) >> 4 + currchar |= (currchunk[2] & 0b11) << 4 + result += chr(0x20 + currchar) + currchar = (currchunk[2] & 0b11111100) >> 2 + result += chr(0x20 + currchar) + return result + + +def decode_fru_date(datebytes): + # Returns ISO + datebytes.append(0) + minutesfromepoch = struct.unpack(' frusize: + chunksize = frusize + offset = 0 + self.rawfru = bytearray([]) + while chunksize: + response = await self.ipmicmd.raw_command( + netfn=0xa, command=0x11, data=[fruid, offset & 0xff, + offset >> 8, chunksize]) + if response['code'] in (201, 202): + # if it was too big, back off and try smaller + # Try just over half to mitigate the chance of + # one request becoming three rather than just two + if chunksize == 3: + raise iexc.IpmiException(response['error']) + chunksize //= 2 + chunksize += 2 + continue + elif 'error' in response: + raise iexc.IpmiException(response['error'], response['code']) + offset += response['data'][0] + if response['data'][0] == 0: + break + # move down to avoid exception when data[0] is zero + self.rawfru.extend(response['data'][1:]) + if offset + chunksize > frusize: + chunksize = frusize - offset + + def parsedata(self): + self.info = {} + rawdata = self.rawfru + self.databytes = bytearray(rawdata) + if self.sdr is not None: + frutype = self.sdr.fru_type_and_modifier >> 8 + frusubtype = self.sdr.fru_type_and_modifier & 0xff + if frutype > 0x10 or frutype < 0x8 or frusubtype not in (0, 1, 2): + return + # TODO(jjohnson2): strict mode to detect aiohmi and BMC + # gaps + # raise iexc.PyghmiException( + # 'Unsupported FRU device: {0:x}h, {1:x}h'.format(frutype, + # frusubtype + # )) + elif frusubtype == 1: + self.myspd = spd.SPD(self.databytes) + self.info = self.myspd.info + return + if self.databytes[0] != 1: + return + # TODO(jjohnson2): strict mode to flag potential BMC errors + # raise iexc.BmcErrorException("Invalid/Unsupported FRU format") + # Ignore the internal use even if present. + self._parse_chassis() + self._parse_board() + self._parse_prod() + # TODO(jjohnson2): Multi Record area + + def _decode_tlv(self, offset, lang=0): + currtlv = self.databytes[offset] + currlen = currtlv & 0b111111 + currtype = (currtlv & 0b11000000) >> 6 + retinfo = self.databytes[offset + 1:offset + currlen + 1] + newoffset = offset + currlen + 1 + if currlen == 0: + return None, newoffset + if currtype == 0: + # return it as a bytearray, not much to be done for it + return retinfo, newoffset + elif currtype == 3: # text string + # Sometimes BMCs have FRU data with 0xff termination + # contrary to spec, but can be tolerated + # also in case something null terminates, handle that too + # strictly speaking, \xff should be a y with diaeresis, but + # erring on the side of that not being very relevant in practice + # to fru info, particularly the last values + # Additionally 0xfe has been observed, which should be a thorn, but + # again assuming termination of string is more likely than thorn. + retinfo = retinfo.rstrip(b'\xfe\xff\x10\x03\x00 ') + retinfo = retinfo.replace(b'\x00', b'') + if lang in (0, 25): + try: + retinfo = retinfo.decode('iso-8859-1') + except (UnicodeError, LookupError): + pass + else: + try: + retinfo = retinfo.decode('utf-16le') + except (UnicodeDecodeError, LookupError): + pass + # Some things lie about being text. Do the best we can by + # removing trailing spaces and nulls like makes sense for text + # and rely on vendors to workaround deviations in their OEM + # module + # retinfo = retinfo.rstrip(b'\x00 ') + return retinfo, newoffset + elif currtype == 1: # BCD 'plus' + retdata = '' + for byte in retinfo: + byte = hex(byte).replace('0x', '').replace('a', ' ').replace( + 'b', '-').replace('c', '.') + retdata += byte + retdata = retdata.strip() + return retdata, newoffset + elif currtype == 2: # 6-bit ascii + retinfo = unpack6bitascii(retinfo).strip() + return retinfo, newoffset + + def _parse_chassis(self): + offset = 8 * self.databytes[2] + if offset == 0: + return + if self.databytes[offset] & 0b1111 != 1: + raise iexc.BmcErrorException("Invalid/Unsupported chassis area") + inf = self.info + # ignore length field, just process the data + # add check to avoid exception + if self.databytes[offset + 2] in enclosure_types.keys(): + inf['Chassis type'] = enclosure_types[self.databytes[offset + 2]] + inf['Chassis part number'], offset = self._decode_tlv(offset + 3) + inf['Chassis serial number'], offset = self._decode_tlv(offset) + inf['chassis_extra'] = [] + self.extract_extra(inf['chassis_extra'], offset) + + def extract_extra(self, target, offset, language=0): + try: + while self.databytes[offset] != 0xc1: + fielddata, offset = self._decode_tlv(offset, language) + target.append(fielddata) + except IndexError: + # If we overrun the end due to malformed FRU, + # return at least what decoded right + return + + def _parse_board(self): + offset = 8 * self.databytes[3] + if offset == 0: + return + if self.databytes[offset] & 0b1111 != 1: + raise iexc.BmcErrorException("Invalid/Unsupported board info area") + inf = self.info + language = self.databytes[offset + 2] + inf['Board manufacture date'] = decode_fru_date( + self.databytes[offset + 3:offset + 6]) + inf['Board manufacturer'], offset = self._decode_tlv(offset + 6) + inf['Board product name'], offset = self._decode_tlv(offset, language) + inf['Board serial number'], offset = self._decode_tlv(offset, language) + inf['Board model'], offset = self._decode_tlv(offset, language) + inf['Board FRU Id'], offset = self._decode_tlv(offset, language) + inf['board_extra'] = [] + self.extract_extra(inf['board_extra'], offset, language) + + def _parse_prod(self): + offset = 8 * self.databytes[4] + if offset == 0: + return + inf = self.info + language = self.databytes[offset + 2] + inf['Manufacturer'], offset = self._decode_tlv(offset + 3, + language) + inf['Product name'], offset = self._decode_tlv(offset, language) + inf['Model'], offset = self._decode_tlv(offset, language) + inf['Hardware Version'], offset = self._decode_tlv(offset, language) + inf['Serial Number'], offset = self._decode_tlv(offset, language) + inf['Asset Number'], offset = self._decode_tlv(offset, language) + inf['FRU ID'], offset = self._decode_tlv(offset, language) + inf['product_extra'] = [] + self.extract_extra(inf['product_extra'], offset, language) + + def __repr__(self): + return repr(self.info) + # retdata = 'Chassis data\n' + # retdata += ' Type: ' + repr(self.chassis_type) + '\n' + # retdata += ' Part Number: ' + repr(self.chassis_part_number) + '\n' + # retdata += ' Serial Number: ' + repr(self.chassis_serial) + '\n' + # retdata += ' Extra: ' + repr(self.chassis_extra) + '\n' + # retdata += 'Board data\n' + # retdata += ' Manufacturer: ' + repr(self.board_manufacturer) + '\n' + # retdata += ' Date: ' + repr(self.board_mfg_date) + '\n' + # retdata += ' Product' + repr(self.board_product) + '\n' + # retdata += ' Serial: ' + repr(self.board_serial) + '\n' + # retdata += ' Model: ' + repr(self.board_model) + '\n' + # retdata += ' Extra: ' + repr(self.board_extra) + '\n' + # retdata += 'Product data\n' + # retdata += ' Manufacturer: ' + repr(self.product_manufacturer)+'\n' + # retdata += ' Name: ' + repr(self.product_name) + '\n' + # retdata += ' Model: ' + repr(self.product_model) + '\n' + # retdata += ' Version: ' + repr(self.product_version) + '\n' + # retdata += ' Serial: ' + repr(self.product_serial) + '\n' + # retdata += ' Asset: ' + repr(self.product_asset) + '\n' + # retdata += ' Extra: ' + repr(self.product_extra) + '\n' + # return retdata diff --git a/confluent_server/aiohmi/ipmi/oem/__init__.py b/confluent_server/aiohmi/ipmi/oem/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/confluent_server/aiohmi/ipmi/oem/generic.py b/confluent_server/aiohmi/ipmi/oem/generic.py new file mode 100644 index 00000000..34ca2b79 --- /dev/null +++ b/confluent_server/aiohmi/ipmi/oem/generic.py @@ -0,0 +1,550 @@ +# Copyright 2015 Lenovo 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. + +import aiohmi.exceptions as exc +import aiohmi.ipmi.private.constants as event_const +import aiohmi.ipmi.sdr as ipmisdr +import struct + +class OEMHandler(object): + """Handler class for OEM capabilities. + + Any vendor wishing to implement OEM extensions should look at this + base class for an appropriate interface. If one does not exist, this + base class should be extended. At initialization an OEM is given + a dictionary with product_id, device_id, manufacturer_id, and + device_revision as keys in a dictionary, along with an ipmi Command object + """ + + @classmethod + async def create(cls, oemid, ipmicmd): + self = cls() + return self + + async def get_video_launchdata(self): + return {} + + async def get_description(self): + """Get a description of descriptive attributes of a node. + + Height describes, in U how tall the system is, and slot is 0 if + not a blade type server, and slot if it is. + + :return: dictionary with 'height' and 'slot' members + """ + return {} + + async def get_screenshot(self, outfile): + return {} + + async def get_system_power_watts(self, ipmicmd): + # Use DCMI getpower reading command + rsp = await ipmicmd.raw_command(netfn=0x2c, command=2, data=(0xdc, 1, 0, 0)) + wattage = struct.unpack(' 1: + raise Exception('TODO: how to deal with multiple inlets') + self._inlet_name = airinlets[0] + elif extenv: + if len(extenv) > 1: + raise Exception('TODO: how to deal with multiple external environments') + self._inlet_name = extenv[0] + if not self._inlet_name: + raise exc.UnsupportedFunctionality( + 'Unable to detect inlet sensor name for this platform') + return await ipmicmd.get_sensor_reading(self._inlet_name) + + async def process_event(self, event, ipmicmd, seldata): + """Modify an event according with OEM understanding. + + Given an event, allow an OEM module to augment it. For example, + event data fields can have OEM bytes. Other times an OEM may wish + to apply some transform to some field to suit their conventions. + """ + event['oem_handler'] = None + evdata = event['event_data_bytes'] + if evdata[0] & 0b11000000 == 0b10000000: + event['oem_byte2'] = evdata[1] + if evdata[0] & 0b110000 == 0b100000: + event['oem_byte3'] = evdata[2] + + async def clear_system_configuration(self): + raise exc.UnsupportedFunctionality( + 'Clearing system configuration not implemented for this platform') + + async def clear_bmc_configuration(self): + raise exc.UnsupportedFunctionality( + 'Clearing BMC configuration not implemented for this platform') + + async def get_oem_inventory_descriptions(self): + """Get descriptions of available additional inventory items + + OEM implementation may provide additional records not indicated + by FRU locator SDR records. An implementation is expected to + implement this function to list component names that would map to + OEM behavior beyond the specification. It should return an iterable + of names + """ + if False: + yield None + + async def get_sensor_reading(self, sensorname): + """Get an OEM sensor + + If software wants to model some OEM behavior as a 'sensor' without + doing SDR, this hook provides that ability. It should mimic + the behavior of 'get_sensor_reading' in command.py. + """ + raise Exception('Sensor not found: ' + sensorname) + + async def get_sensor_descriptions(self): + """Get list of OEM sensor names and types + + Iterate over dicts describing a label and type for OEM 'sensors'. This + should mimic the behavior of the get_sensor_descriptions function + in command.py. + """ + if False: + yield None + + async def get_diagnostic_data(self, savefile, progress=None): + """Download diagnostic data about target to a file + + This should be a payload that the vendor's support team can use + to do diagnostics. + :param savefile: File object or filename to save to + :param progress: Callback to be informed about progress + :return: + """ + raise exc.UnsupportedFunctionality( + 'Do not know how to get diagnostic data for this platform') + + async def get_sensor_data(self): + """Get OEM sensor data + + Iterate through all OEM 'sensors' and return data as if they were + normal sensors. This should mimic the behavior of the get_sensor_data + function in command.py. + """ + if False: + yield None + + async def get_oem_inventory(self): + """Get tuples of component names and inventory data. + + This returns an iterable of tuples. The first member of each tuple + is a string description of the inventory item. The second member + is a dict of inventory information about the component. + """ + async for desc in self.get_oem_inventory_descriptions(): + yield (desc, await self.get_inventory_of_component(desc)) + + async def get_inventory_of_component(self, component): + """Get inventory detail of an OEM defined component + + Given a string that may be an OEM component, return the detail of that + component. If the component does not exist, returns None + """ + return None + + async def get_leds(self): + """Get tuples of LED categories. + + Each category contains a category name and a dicionary of LED names + with their status as values. + """ + if False: + yield None + + async def get_ntp_enabled(self): + """Get whether ntp is enabled or not + + :returns: True if enabled, False if disabled, None if unsupported + """ + return None + + async def set_ntp_enabled(self, enabled): + """Set whether NTP should be enabled + + :returns: True on success + """ + return None + + async def get_ntp_servers(self): + """Get current set of configured NTP servers + + :returns iterable of configured NTP servers: + """ + return () + + async def set_ntp_server(self, server, index=0): + """Set an ntp server + + :param server: Destination address of server to reach + :param index: Index of server to configure, primary assumed if not + specified + :returns: True if success + """ + return None + + async def process_fru(self, fru, name=None): + """Modify a fru entry with OEM understanding. + + Given a fru, clarify 'extra' fields according to OEM rules and + return the transformed data structure. If OEM processes, it is + expected that it sets 'oem_parser' to the name of the module. For + clients passing through data, it is suggested to pass through + board/product/chassis_extra_data arrays if 'oem_parser' is None, + and mask those fields if not None. It is expected that OEMs leave + the fields intact so that if client code hard codes around the + ordered lists that their expectations are not broken by an update. + """ + # In the generic case, just pass through + if fru is None: + return fru + fru['oem_parser'] = None + return fru + + async def get_oem_firmware(self, bmcver, components, category): + """Get Firmware information.""" + + # Here the bmc version is passed into the OEM handler, to allow + # the handler to enrich the data. For the generic case, just + # provide the generic BMC version, which is all that is possible + # Additionally, components may be provided for an advisory guide + # on interesting firmware. The OEM library is permitted to return + # more than requested, and it is the responsibility of the calling + # code to know whether it cares or not. The main purpose of the + # components argument is to indicate when certain performance + # optimizations can be performed. + yield 'BMC Version', {'version': bmcver} + + async def get_oem_capping_enabled(self): + """Get PSU based power capping status + + :return: True if enabled and False if disabled + """ + return () + + async def set_oem_capping_enabled(self, enable): + """Set PSU based power capping + + :param enable: True for enable and False for disable + """ + return () + + async def get_oem_remote_kvm_available(self): + """Get remote KVM availability""" + return False + + async def get_oem_domain_name(self): + """Get Domain name""" + return () + + async def set_oem_domain_name(self, name): + """Set Domain name + + :param name: domain name to be set + """ + return () + + async def clear_storage_arrays(self): + raise exc.UnsupportedFunctionality( + 'Remote storage configuration not supported on this platform') + + async def remove_storage_configuration(self, cfgspec): + raise exc.UnsupportedFunctionality( + 'Remote storage configuration not supported on this platform') + + async def apply_storage_configuration(self, cfgspec): + raise exc.UnsupportedFunctionality( + 'Remote storage configuration not supported on this platform') + + async def check_storage_configuration(self, cfgspec): + raise exc.UnsupportedFunctionality( + 'Remote storage configuration not supported on this platform') + + async def get_storage_configuration(self): + raise exc.UnsupportedFunctionality( + 'Remote storage configuration not supported on this platform') + + async def get_update_status(self): + raise exc.UnsupportedFunctionality( + 'Firmware update not supported on this platform') + + async def update_firmware(self, filename, data=None, progress=None, bank=None): + raise exc.UnsupportedFunctionality( + 'Firmware update not supported on this platform') + + async def reseat_bay(self, bay): + raise exc.UnsupportedFunctionality( + 'Reseat not supported on this platform') + + async def get_graphical_console(self): + """Get graphical console launcher""" + return () + + async def add_extra_net_configuration(self, netdata, channel=None): + """Add additional network configuration data + + Given a standard netdata struct, add details as relevant from + OEM commands, modifying the passed dictionary + :param netdata: Dictionary to store additional network data + """ + return + + async def get_oem_identifier(self): + """Get host name + + """ + return None + + async def set_oem_identifier(self, name): + """Set host name + + :param name: host name to be set + """ + return False + + async def detach_remote_media(self): + raise exc.UnsupportedFunctionality() + + async def attach_remote_media(self, imagename, username, password): + raise exc.UnsupportedFunctionality() + + async def upload_media(self, filename, progress, data): + raise exc.UnsupportedFunctionality( + 'Remote media upload not supported on this system') + + async def list_media(self): + if False: + yield None + raise exc.UnsupportedFunctionality() + + async def set_identify(self, on, duration, blink): + """Provide an OEM override for set_identify + + Some systems may require an override for set identify. + + """ + raise exc.UnsupportedFunctionality() + + async def get_health(self, summary): + """Provide an alternative or augmented health assessment + + An OEM handler can preprocess the summary and extend it with OEM + specific data, and then return to let generic processing occur. + It can also raise the aiohmi exception BypassGenericBehavior to + suppress the standards based routine, for enhanced performance. + + :param summary: The health summary as prepared by the generic function + :return: Nothing, modifies the summary object + """ + return [] + + async def set_hostname(self, hostname): + """OEM specific hook to specify name information""" + raise exc.UnsupportedFunctionality() + + async def get_hostname(self): + """OEM specific hook to specify name information""" + raise exc.UnsupportedFunctionality() + + async def set_user_access(self, uid, channel, callback, link_auth, ipmi_msg, + privilege_level): + if privilege_level.startswith('custom.'): + raise exc.UnsupportedFunctionality() + return # Nothing to do + + async def set_alert_ipv6_destination(self, ip, destination, channel): + """Set an IPv6 alert destination + + If and only if an implementation does not support standard + IPv6 but has an OEM implementation, override this to process + the data. + + :param ip: IPv6 address to set + :param destination: Destination number + :param channel: Channel number to apply + + :returns True if standard parameter set should be suppressed + """ + return False + + async def get_extended_bmc_configuration(self): + """Get extended bmc configuration + + In the case of potentially redundant/slow + attributes, retrieve unpopular options that may be + redundant or confusing and slow. + """ + return {} + + async def get_bmc_configuration(self): + """Get additional BMC parameters + + This allows a bmc to return arbitrary key-value pairs. + """ + return {} + + async def set_bmc_configuration(self, changeset): + raise exc.UnsupportedFunctionality( + 'Platform does not support setting bmc attributes') + + async def get_system_configuration(self, hideadvanced): + """Retrieve system configuration + + This returns a dictionary of settings names to dictionaries including + 'current', 'default' and 'possible' values as well as 'help' + + :param hideadvanced: Whether to hide 'advanced' settings that most + users should not need. Defaults to True. + """ + return {} + + async def set_system_configuration(self, changeset): + """Apply a changeset to system configuration + + Takes a key value pair and applies it against the system configuration + """ + raise exc.UnsupportedFunctionality() + + async def get_licenses(self): + raise exc.UnsupportedFunctionality() + yield None + + async def delete_license(self, name): + raise exc.UnsupportedFunctionality() + + async def save_licenses(self, directory): + raise exc.UnsupportedFunctionality() + yield None + + async def apply_license(self, filename, progress=None, data=None): + raise exc.UnsupportedFunctionality() + yield None + + async def get_user_expiration(self, uid): + return None + + async def get_user_privilege_level(self, uid): + return None + + async def set_oem_extended_privilleges(self, uid): + """Set user extended privillege as 'KVM & VMedia Allowed' + + |KVM & VMedia Not Allowed 0x00 0x00 0x00 0x00 + |KVM Only Allowed 0x01 0x00 0x00 0x00 + |VMedia Only Allowed 0x02 0x00 0x00 0x00 + |KVM & VMedia Allowed 0x03 0x00 0x00 0x00 + + :param uid: User ID. + """ + return False + + async def process_zero_fru(self, zerofru): + return await self.process_fru(zerofru) + + async def is_valid(self, name): + return name is not None + + async def process_password(self, password, data): + return data + + async def set_server_capping(self, value): + """Set power capping for server + + :param value: power capping value to set. + """ + pass + + async def get_server_capping(self): + """Get power capping for server + + :return: power capping value. + """ + return None + + async def get_oem_event_const(self): + return event_const diff --git a/confluent_server/aiohmi/ipmi/oem/lenovo/__init__.py b/confluent_server/aiohmi/ipmi/oem/lenovo/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/confluent_server/aiohmi/ipmi/oem/lenovo/config.py b/confluent_server/aiohmi/ipmi/oem/lenovo/config.py new file mode 100644 index 00000000..1e5a54a9 --- /dev/null +++ b/confluent_server/aiohmi/ipmi/oem/lenovo/config.py @@ -0,0 +1,637 @@ +# Copyright 2017-2019 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. + +"""from Matthew Garret's 'firmware_config' project. + +This contains functions to manage the firmware configuration of Lenovo servers +""" + +import ast +import asyncio +import base64 +import random +import struct + +import time + +import aiohmi.exceptions as pygexc + +try: + import EfiCompressor + from lxml import etree +except ImportError: + etree = None + EfiCompressor = None + +IMM_NETFN = 0x2e +IMM_COMMAND = 0x90 +LENOVO_ENTERPRISE = [0x4d, 0x4f, 0x00] + +OPEN_RO_COMMAND = [0x01, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40] +OPEN_WO_COMMAND = [0x01, 0x03, 0x01] +READ_COMMAND = [0x02] +WRITE_COMMAND = [0x03] +CLOSE_COMMAND = [0x05] +SIZE_COMMAND = [0x06] + +class Unsupported(Exception): + pass + +def fromstring(inputdata): + if b'!entity' in inputdata.lower(): + raise Exception('Unsupported XML') + try: + return etree.fromstring(inputdata) + except etree.XMLSyntaxError: + inputdata = bytearray(inputdata.decode('utf8', errors='backslashreplace').encode()) + for i in range(len(inputdata)): + if inputdata[i] < 0x20 and inputdata[i] not in (9, 0xa, 0xd): + inputdata[i] = 63 + inputdata = bytes(inputdata) + return etree.fromstring(inputdata) + + + +async def run_command_with_retry(connection, data): + tries = 240 + while tries: + tries -= 1 + try: + return await connection.raw_command( + netfn=IMM_NETFN, command=IMM_COMMAND, data=data) + except pygexc.IpmiException as e: + if e.ipmicode != 0xa or not tries: + raise + connection.ipmi_session.pause(1) + + +def _convert_syntax(raw): + return raw.replace('!', 'not').replace('||', 'or').replace( + '&&', 'and').replace('-', '_') + + +class _ExpEngine(object): + def __init__(self, cfg, setting): + self.cfg = cfg + self.setting = setting + self.relatedsettings = set([]) + + def lookup(self, category, setting): + for optkey in self.cfg: + opt = self.cfg[optkey] + lid = opt['lenovo_id'].replace('-', '_') + if (lid == category + and opt['lenovo_setting'] == setting): + self.relatedsettings.add(optkey) + return opt['lenovo_value'] + return None + + def process(self, parsed): + if isinstance(parsed, ast.UnaryOp) and isinstance(parsed.op, ast.Not): + return not self.process(parsed.operand) + if isinstance(parsed, ast.Compare): + if isinstance(parsed.ops[0], ast.NotEq): + return self.process(parsed.left) != self.process( + parsed.comparators[0]) + elif isinstance(parsed.ops[0], ast.Eq): + return self.process(parsed.left) == self.process( + parsed.comparators[0]) + if isinstance(parsed, ast.Num): + return parsed.n + if isinstance(parsed, ast.Attribute): + category = parsed.value.id + setting = parsed.attr + return self.lookup(category, setting) + if isinstance(parsed, ast.Name): + if parsed.id == 'true': + return True + elif parsed.id == 'false': + return False + else: + category = self.setting['lenovo_id'] + setting = parsed.id + return self.lookup(category, setting) + if isinstance(parsed, ast.BoolOp): + if isinstance(parsed.op, ast.Or): + return self.process(parsed.values[0]) or self.process( + parsed.values[1]) + elif isinstance(parsed.op, ast.And): + return self.process(parsed.values[0]) and self.process( + parsed.values[1]) + + +def _eval_conditional(expression, cfg, setting): + if not expression: + return False, () + try: + parsed = ast.parse(expression) + parsed = parsed.body[0].value + evaluator = _ExpEngine(cfg, setting) + result = evaluator.process(parsed) + return result, evaluator.relatedsettings + except SyntaxError: + return False, () + + +class LenovoFirmwareConfig(object): + def __init__(self, xc, useipmi=True): + if not etree: + raise Exception("python-lxml and python-eficompressor required " + "for this function") + if useipmi: + self.connection = xc.ipmicmd + else: + self.connection = None + self.xc = xc + + async def imm_size(self, filename): + data = bytearray() + data.extend(LENOVO_ENTERPRISE) + data.extend(SIZE_COMMAND) + if not isinstance(filename, bytes): + filename = filename.encode('utf-8') + data.extend(filename) + + response = await run_command_with_retry(self.connection, data=data) + + size = response['data'][3:7] + + size = struct.unpack("i", size) + return size[0] + + async def imm_open(self, filename, write=False, size=None): + response = None + retries = 12 + data = bytearray() + data.extend(LENOVO_ENTERPRISE) + if write is False: + data.extend(OPEN_RO_COMMAND) + else: + assert size is not None + data.extend(OPEN_WO_COMMAND) + hex_size = struct.pack(" 0: + data = bytearray() + data.extend(LENOVO_ENTERPRISE) + data.extend(WRITE_COMMAND) + data.extend(hex_filehandle[:4]) + hex_offset = struct.pack(" 0: + data = [] + data += LENOVO_ENTERPRISE + data += READ_COMMAND + data.extend(bytearray(hex_filehandle[:4])) + hex_offset = struct.pack(" 0: + for order in sorted(currentdict): + current.append(currentdict[order]) + if len(currentdef) > 0: + default = [] + for order in sorted(currentdef): + default.append(currentdef[order]) + optionname = "%s.%s" % (cfglabel, name) + alias = "%s.%s" % (lenovo_id, name) + if onedata is not None: + if current and len(current) > 1: + instidx = 1 + for inst in current: + if currentidxes: + instidx = currentidxes.pop(0) + optname = '{0}.{1}'.format(optionname, instidx) + options[optname] = dict( + current=inst, + default=default, + possible=possible, + pending=None, + new_value=None, + help=help, + is_list=is_list, + lenovo_value=lenovo_value, + lenovo_id=lenovo_id, + lenovo_group=lenovo_group, + lenovo_setting=lenovo_setting, + lenovo_reboot=reset, + lenovo_protect=protect, + lenovo_instance=instidx, + readonly_expression=readonly, + hide_expression=hide, + sortid=sortid, + validexpression=validexpression, + alias=alias) + sortid += 1 + instidx += 1 + continue + if current: + current = current[0] + if instancetochoicemap: + for currid in sorted(instancetochoicemap): + optname = '{0}.{1}'.format(optionname, currid) + current = instancetochoicemap[currid] + options[optname] = dict( + current=current, + default=default, + possible=possible, + pending=None, + new_value=None, + help=help, + is_list=is_list, + lenovo_value=lenovo_value, + lenovo_id=lenovo_id, + lenovo_group=lenovo_group, + lenovo_setting=lenovo_setting, + lenovo_reboot=reset, + lenovo_protect=protect, + lenovo_instance=currid, + readonly_expression=readonly, + hide_expression=hide, + sortid=sortid, + validexpression=validexpression, + alias=alias) + sortid += 1 + continue + lenovoinstance = "" + if forceinstance: + optionname = '{0}.{1}'.format(optionname, 1) + lenovoinstance = 1 + options[optionname] = dict(current=current, + default=default, + possible=possible, + pending=None, + new_value=None, + help=help, + is_list=is_list, + lenovo_value=lenovo_value, + lenovo_id=lenovo_id, + lenovo_group=lenovo_group, + lenovo_setting=lenovo_setting, + lenovo_reboot=reset, + lenovo_protect=protect, + lenovo_instance=lenovoinstance, + readonly_expression=readonly, + hide_expression=hide, + sortid=sortid, + validexpression=validexpression, + alias=alias) + sortid = sortid + 1 + for opt in options: + opt = options[opt] + opt['hidden'], opt['hidden_why'] = _eval_conditional( + opt['hide_expression'], options, opt) + opt['readonly'], opt['readonly_why'] = _eval_conditional( + opt['readonly_expression'], options, opt) + + return options + + async def set_fw_options(self, options, checkonly=False): + changes = False + random.seed() + ident = 'ASU-%x-%x-%x-0' % (random.getrandbits(48), + random.getrandbits(32), + random.getrandbits(64)) + + configurations = etree.Element('configurations', ID=ident, + type='update', update='ASU Client') + + for option in options.keys(): + if options[option]['new_value'] is None: + continue + if options[option]['readonly']: + errstr = '{0} is read only'.format(option) + if options[option]['readonly_why']: + ea = ' due to one of the following settings: {0}'.format( + ','.join(sorted(options[option]['readonly_why']))) + errstr += ea + raise pygexc.InvalidParameterValue(errstr) + if options[option]['current'] == options[option]['new_value']: + continue + if options[option]['pending'] == options[option]['new_value']: + continue + if isinstance(options[option]['new_value'], str): + # Coerce a simple string parameter to the expected list format + options[option]['new_value'] = [options[option]['new_value']] + options[option]['pending'] = options[option]['new_value'] + + is_list = options[option]['is_list'] + count = 0 + changes = True + config = etree.Element('config', ID=options[option]['lenovo_id']) + configurations.append(config) + setting = etree.Element('setting', + ID=options[option]['lenovo_setting']) + if options[option]['lenovo_group'] is not None: + group = etree.Element('group', + ID=options[option]['lenovo_group']) + config.append(group) + group.append(setting) + else: + config.append(setting) + if is_list: + container = etree.Element('list_data') + setting.append(container) + else: + container = etree.Element('enumerate_data') + setting.append(container) + + for value in options[option]['new_value']: + choice = etree.Element('choice') + container.append(choice) + label = etree.Element('label') + label.text = value + choice.append(label) + if is_list: + count += 1 + instance = etree.Element( + 'instance', ID=str(options[option]['lenovo_instance']), + order=str(count)) + else: + instance = etree.Element( + 'instance', ID=str(options[option]['lenovo_instance'])) + choice.append(instance) + + if not changes: + return False + if checkonly: + return True + + xml = etree.tostring(configurations) + data = EfiCompressor.FrameworkCompress(xml, len(xml)) + bdata = base64.b64encode(data).decode('utf8') + rsp = await self.xc.grab_redfish_response_with_status( + '/redfish/v1/Managers/1') + if rsp[1] == 200: + if 'purley' not in rsp[0].get('Oem', {}).get('Lenovo', {}).get( + 'release_name', 'purley'): + rsp = await self.xc.grab_redfish_response_with_status( + '/redfish/v1/Systems/1/Actions/Oem/' + 'LenovoComputerSystem.DSWriteFile', + {'Action': 'DSWriteFile', 'Resize': len(data), + 'FileName': 'asu_update.efi', 'Content': bdata}) + if rsp[1] == 204: + return True + if self.connection is None: + raise Unsupported('Not Supported') + filehandle = await self.imm_open("asu_update.efi", write=True, + size=len(data)) + await self.imm_write(filehandle, len(data), data) + stubread = len(data) + if stubread > 8: + stubread = 8 + await self.imm_read(filehandle, stubread) + await self.imm_close(filehandle) + return True diff --git a/confluent_server/aiohmi/ipmi/oem/lenovo/cpu.py b/confluent_server/aiohmi/ipmi/oem/lenovo/cpu.py new file mode 100755 index 00000000..e3e98910 --- /dev/null +++ b/confluent_server/aiohmi/ipmi/oem/lenovo/cpu.py @@ -0,0 +1,53 @@ +# 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. + +from aiohmi.ipmi.oem.lenovo import inventory + +cpu_fields = ( + inventory.EntryField("index", "B"), + inventory.EntryField("Cores", "B"), + inventory.EntryField("Threads", "B"), + inventory.EntryField("Manufacturer", "13s"), + inventory.EntryField("Family", "30s"), + inventory.EntryField("Model", "30s"), + inventory.EntryField("Stepping", "3s"), + inventory.EntryField("Maximum Frequency", "I", + valuefunc=lambda v: hex(v)[2:]), + inventory.EntryField("model", "21s"), + inventory.EntryField("reserved", "h", include=False) +) + +dimm_cmd = { + "lenovo": { + "netfn": 0x06, + "command": 0x59, + "data": (0x00, 0xc1, 0x02, 0x00)}, + "asrock": { + "netfn": 0x3a, + "command": 0x50, + "data": (0x01, 0x02, 0x01)}, +} + + +def parse_dimm_info(raw): + return inventory.parse_inventory_category_entry(raw, dimm_fields) + + +def get_categories(): + return { + "dimm": { + "idstr": "DIMM {0}", + "parser": parse_dimm_info, + "command": dimm_cmd, + "workaround_bmc_bug": lambda t: True + } + } diff --git a/confluent_server/aiohmi/ipmi/oem/lenovo/drive.py b/confluent_server/aiohmi/ipmi/oem/lenovo/drive.py new file mode 100755 index 00000000..7f57119e --- /dev/null +++ b/confluent_server/aiohmi/ipmi/oem/lenovo/drive.py @@ -0,0 +1,69 @@ +# 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. + +from aiohmi.ipmi.oem.lenovo import inventory + +drive_fields = ( + inventory.EntryField("index", "B"), + inventory.EntryField("VendorID", "64s"), + inventory.EntryField("Size", "I", + valuefunc=lambda v: str(v) + " MB"), + inventory.EntryField("MediaType", "B", mapper={ + 0x00: "HDD", + 0x01: "SSD" + }), + inventory.EntryField("InterfaceType", "B", mapper={ + 0x00: "Unknown", + 0x01: "ParallelSCSI", + 0x02: "SAS", + 0x03: "SATA", + 0x04: "FC" + }), + inventory.EntryField("FormFactor", "B", mapper={ + 0x00: "Unknown", + 0x01: "2.5in", + 0x02: "3.5in" + }), + inventory.EntryField("LinkSpeed", "B", mapper={ + 0x00: "Unknown", + 0x01: "1.5 Gb/s", + 0x02: "3.0 Gb/s", + 0x03: "6.0 Gb/s", + 0x04: "12.0 Gb/s" + }), + inventory.EntryField("SlotNumber", "B"), + inventory.EntryField("ControllerIndex", "B"), + inventory.EntryField("DeviceState", "B", mapper={ + 0x00: "active", + 0x01: "stopped", + 0xff: "transitioning" + })) + + +def parse_drive_info(raw): + return inventory.parse_inventory_category_entry(raw, drive_fields) + + +def get_categories(): + return { + "drive": { + "idstr": "Drive {0}", + "parser": parse_drive_info, + "command": { + "netfn": 0x06, + "command": 0x59, + "data": (0x00, 0xc1, 0x04, 0x00) + } + } + } diff --git a/confluent_server/aiohmi/ipmi/oem/lenovo/energy.py b/confluent_server/aiohmi/ipmi/oem/lenovo/energy.py new file mode 100644 index 00000000..f2469308 --- /dev/null +++ b/confluent_server/aiohmi/ipmi/oem/lenovo/energy.py @@ -0,0 +1,166 @@ +# Copyright 2017 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. + +import struct + +import aiohmi.constants as const +import aiohmi.exceptions as pygexc +import aiohmi.ipmi.sdr as sdr + + +class EnergyManager(object): + + @classmethod + async def create(cls, ipmicmd): + # there are two IANA possible for the command set, start with + # the Lenovo, then fallback to IBM + # We start with a 'find firmware instance' to test the water and + # get the handle (which has always been the same, but just in case + self = cls() + self.iana = bytearray(b'\x66\x4a\x00') + self._usefapm = False + self._mypowermeters = () + try: + rsp = await ipmicmd.raw_command(netfn=0x3a, command=0x32, data=[4, 2, 0, 0, 0]) + if len(rsp['data']) >= 8: + self.supportedmeters = ('DC Energy', 'GPU Power', + 'Node Power', 'Total Power') + self._mypowermeters = ('node power', 'total power', 'gpu power', 'riser 1 power', 'riser 2 power') + self._usefapm = True + return + except pygexc.IpmiException: + pass + + try: + rsp = await ipmicmd.raw_command(netfn=0x2e, command=0x82, + data=self.iana + b'\x00\x00\x01') + except pygexc.IpmiException as ie: + if ie.ipmicode == 193: # try again with IBM IANA + self.iana = bytearray(b'\x4d\x4f\x00') + rsp = await ipmicmd.raw_command(netfn=0x2e, command=0x82, + data=self.iana + b'\x00\x00\x01') + else: + raise + if rsp['data'][4:6] not in (b'\x02\x01', b'\x02\x06', b'\x02\x09'): + raise pygexc.UnsupportedFunctionality( + "Energy Control {0}.{1} not recognized".format(rsp['data'][4], + rsp['data'][5])) + self.modhandle = bytearray(rsp['data'][6:7]) + if await self.get_ac_energy(ipmicmd): + self.supportedmeters = ('AC Energy', 'DC Energy') + else: + self.supportedmeters = ('DC Energy',) + return self + + def supports(self, name): + if name.lower() in self._mypowermeters: + return True + return False + + async def get_sensor(self, name, ipmicmd): + if name.lower() not in self._mypowermeters: + raise pygexc.UnsupportedFunctionality('Unrecogcized sensor') + tries = 3 + rsp = None + while tries: + tries -= 1 + try: + rsp = await ipmicmd.raw_command(netfn=0x3a, command=0x32, data=[4, 8, 0, 0, 0]) + break + except pygexc.IpmiException as ie: + if tries and ie.ipmicode == 0xc3: + ipmicmd.ipmi_session.pause(0.1) + continue + raise + if rsp is None: + raise pygexc.UnsupportedFunctionality('Unrecogcized sensor') + npow, gpupow, r1pow, r2pow = struct.unpack('> 4)) * 10 + \ + (0xf & minor_version) + aux_reversion = 0 + if str(data['Auxiliary Firmware Revision']) != '': + aux_reversion = ord(data['Auxiliary Firmware Revision']) + + bmc_version = "%s.%s.%s" % ( + str(major_version), + str(minor_version), + str(aux_reversion)) + + yield ("BMC", {'version': bmc_version}) + if bios_versions is not None: + yield ("Bios", {'version': bios_versions[0:]}) + else: + del data["Revision"] + for key in data: + yield (key, {'version': data[key]}) + if bios_versions is not None: + yield ("Bios_bundle_ver", + {'version': bios_versions['new_img_version']}) + yield ("Bios_current_ver", + {'version': bios_versions['cur_img_version']}) + + +def parse_bios_number(raw): + return inventory.parse_bios_number_entry(raw) + + +def get_categories(): + return { + "firmware": { + "idstr": "FW Version", + "parser": parse_firmware_info, + "command": firmware_cmd + }, + "bios_version": { + "idstr": "Bios Version", + "parser": parse_bios_number, + "command": bios_cmd + } + } diff --git a/confluent_server/aiohmi/ipmi/oem/lenovo/handler.py b/confluent_server/aiohmi/ipmi/oem/lenovo/handler.py new file mode 100755 index 00000000..6d632419 --- /dev/null +++ b/confluent_server/aiohmi/ipmi/oem/lenovo/handler.py @@ -0,0 +1,1440 @@ +# Copyright 2015-2017 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. + +import base64 +import binascii +import socket +import struct +import traceback +import weakref + +import aiohmi.constants as pygconst +import aiohmi.exceptions as pygexc +import aiohmi.ipmi.oem.generic as generic +from aiohmi.ipmi.oem.lenovo import cpu +from aiohmi.ipmi.oem.lenovo import dimm +from aiohmi.ipmi.oem.lenovo import drive +from aiohmi.ipmi.oem.lenovo import energy +from aiohmi.ipmi.oem.lenovo import firmware +from aiohmi.ipmi.oem.lenovo import imm +from aiohmi.ipmi.oem.lenovo import inventory +from aiohmi.ipmi.oem.lenovo import nextscale +from aiohmi.ipmi.oem.lenovo import pci +from aiohmi.ipmi.oem.lenovo import psu +from aiohmi.ipmi.oem.lenovo import raid_controller +from aiohmi.ipmi.oem.lenovo import raid_drive +import aiohmi.ipmi.private.constants as ipmiconst +import aiohmi.ipmi.private.util as util +import aiohmi.redfish.command as redfishcmd +from aiohmi.redfish.oem.lenovo import tsma +import aiohmi.util.webclient as wc + +try: + from urllib import urlencode +except ImportError: + from urllib.parse import urlencode + +try: + range = xrange +except NameError: + pass +try: + buffer +except NameError: + buffer = memoryview + +inventory.register_inventory_category(cpu) +inventory.register_inventory_category(dimm) +inventory.register_inventory_category(pci) +inventory.register_inventory_category(drive) +inventory.register_inventory_category(psu) +inventory.register_inventory_category(raid_drive) +inventory.register_inventory_category(raid_controller) + + +firmware_types = { + 1: 'Management Controller', + 2: 'UEFI/BIOS', + 3: 'CPLD', + 4: 'Power Supply', + 5: 'Storage Adapter', + 6: 'Add-in Adapter', +} + +firmware_event = { + 0: ('Update failed', pygconst.Health.Failed), + 1: ('Update succeeded', pygconst.Health.Ok), + 2: ('Update aborted', pygconst.Health.Ok), + 3: ('Unknown', pygconst.Health.Warning), +} + +me_status = { + 0: ('Recovery GPIO forced', pygconst.Health.Warning), + 1: ('ME Image corrupt', pygconst.Health.Critical), + 2: ('Flash erase error', pygconst.Health.Critical), + 3: ('Unspecified flash state', pygconst.Health.Warning), + 4: ('ME watchdog timeout', pygconst.Health.Critical), + 5: ('ME platform reboot', pygconst.Health.Critical), + 6: ('ME update', pygconst.Health.Ok), + 7: ('Manufacturing error', pygconst.Health.Critical), + 8: ('ME Flash storage integrity error', pygconst.Health.Critical), + 9: ('ME firmware exception', pygconst.Health.Critical), # event data 3.. + 0xa: ('ME firmware worn', pygconst.Health.Warning), + 0xc: ('Invalid SCMP state', pygconst.Health.Warning), + 0xd: ('PECI over DMI failure', pygconst.Health.Warning), + 0xe: ('MCTP interface failure', pygconst.Health.Warning), + 0xf: ('Auto configuration completed', pygconst.Health.Ok), +} + +me_flash_status = { + 0: ('ME flash corrupted', pygconst.Health.Critical), + 1: ('ME flash erase limit reached', pygconst.Health.Critical), + 2: ('ME flash write limit reached', pygconst.Health.Critical), + 3: ('ME flash write enabled', pygconst.Health.Ok), +} + +leds = { + "BMC_UID": 0x00, + "BMC_HEARTBEAT": 0x01, + "SYSTEM_FAULT": 0x02, + "PSU1_FAULT": 0x03, + "PSU2_FAULT": 0x04, + "LED_FAN_FAULT_1": 0x10, + "LED_FAN_FAULT_2": 0x11, + "LED_FAN_FAULT_3": 0x12, + "LED_FAN_FAULT_4": 0x13, + "LED_FAN_FAULT_5": 0x14, + "LED_FAN_FAULT_6": 0x15, + "LED_FAN_FAULT_7": 0x16, + "LED_FAN_FAULT_8": 0x17 +} + +led_status = { + 0x00: "Off", + 0xFF: "On" +} + +ami_leds = { + "BMC_HEARTBEAT": 0x00, + "BMC_UID": 0x01, + "SYSTEM_FAULT": 0x02, + "HDD_FAULT": 0x03 +} + +asrock_leds = { + "SYSTEM_EVENT": 0x00, + "BMC_UID": 0x01, + "LED_FAN_FAULT_1": 0x02, + "LED_FAN_FAULT_2": 0x03, + "LED_FAN_FAULT_3": 0x04 +} + +ts460_leds = { + "SYSTEM_EVENT": 0x00, + "BMC_UID": 0x01, + "LED_FAN_FAULT_1": 0x02, + "LED_FAN_FAULT_2": 0x03, + "LED_FAN_CPU": 0x04, + "LED_FAN_REAR": 0x05 +} + +asrock_led_status = { + 0x00: "Off", + 0x01: "On" +} + +ami_led_status = { + 0x00: "Off", + 0x01: "On" +} + +led_status_default = "Blink" +mac_format = '{0:02x}:{1:02x}:{2:02x}:{3:02x}:{4:02x}:{5:02x}' +categorie_items = ["cpu", "dimm", "firmware", "bios_version"] + + +def _megarac_abbrev_image(name): + # MegaRAC platform in some places needs an abbreviated filename + # Their scheme in such a scenario is a max of 20. Truncation is + # acheived by taking the first sixteen, then skipping ahead to the last + # 4 (presumably to try to keep '.iso' or '.img' in the name). + if len(name) <= 20: + return name + return name[:16] + name[-4:] + + +class OEMHandler(generic.OEMHandler): + # noinspection PyUnusedLocal + @classmethod + async def create(cls, oemid, ipmicmd): + self = cls() + # will need to retain data to differentiate + # variations. For example System X versus Thinkserver + self.oemid = oemid + self._fpc_variant = None + self.ipmicmd = weakref.proxy(ipmicmd) + self._has_megarac = None + self.oem_inventory_info = None + self._mrethidx = None + self._hasimm = None + self._hasxcc = None + if await self.has_xcc(): + self.immhandler = imm.XCCClient(ipmicmd) + elif await self.has_imm(): + self.immhandler = imm.IMMClient(ipmicmd) + elif await self.is_fpc(): + self.smmhandler = nextscale.SMMClient(ipmicmd, await self.is_fpc()) + elif self.has_tsma: + conn = wc.WebConnection( + ipmicmd.bmc, 443, + verifycallback=self.ipmicmd.certverify) + self.tsmahandler = await tsma.TsmHandler.create(None, None, conn, + fish=redfishcmd) + self.tsmahandler.set_credentials( + ipmicmd.ipmi_session.userid.decode('utf-8'), + ipmicmd.ipmi_session.password.decode('utf-8')) + return self + + + async def _megarac_eth_index(self): + if self._mrethidx is None: + chan = await self.ipmicmd.get_network_channel() + rsp = await self.ipmicmd.raw_command(0x32, command=0x62, data=(chan,)) + self._mrethidx = rsp['data'][0] + return self._mrethidx + + async def get_screenshot(self, outfile): + if await self.has_xcc(): + return await self.immhandler.get_screenshot(outfile) + return {} + + async def remove_storage_configuration(self, cfgspec): + if await self.has_xcc(): + return await self.immhandler.remove_storage_configuration(cfgspec) + return await super(OEMHandler, self).remove_storage_configuration() + + async def get_ikvm_methods(self): + if await self.has_xcc(): + return ['url'] + + async def get_ikvm_launchdata(self): + if await self.has_xcc(): + return await self.immhandler.get_ikvm_launchdata() + return {} + + async def clear_storage_arrays(self): + if await self.has_xcc(): + return await self.immhandler.clear_storage_arrays() + return await super(OEMHandler, self).clear_storage_arrays() + + async def apply_storage_configuration(self, cfgspec): + if await self.has_xcc(): + return await self.immhandler.apply_storage_configuration(cfgspec) + return await super(OEMHandler, self).apply_storage_configuration() + + async def check_storage_configuration(self, cfgspec): + if await self.has_xcc(): + return await self.immhandler.check_storage_configuration(cfgspec) + return await super(OEMHandler, self).get_storage_configuration() + + async def get_storage_configuration(self): + if await self.has_xcc(): + return await self.immhandler.get_storage_configuration() + return await super(OEMHandler, self).get_storage_configuration() + + async def get_video_launchdata(self): + if await self.has_tsm(): + return await self.get_tsm_launchdata() + + async def get_tsm_launchdata(self): + pass + + async def process_event(self, event, ipmicmd, seldata): + if 'oemdata' in event: + oemtype = seldata[2] + oemdata = event['oemdata'] + if oemtype == 0xd0: # firmware update + event['component'] = firmware_types.get(oemdata[0], None) + event['component_type'] = ipmiconst.sensor_type_codes[0x2b] + slotnumber = (oemdata[1] & 0b11111000) >> 3 + if slotnumber: + event['component'] += ' {0}'.format(slotnumber) + event['event'], event['severity'] = \ + firmware_event[oemdata[1] & 0b111] + event['event_data'] = '{0}.{1}'.format(oemdata[2], oemdata[3]) + elif oemtype == 0xd1: # BIOS recovery + event['severity'] = pygconst.Health.Warning + event['component'] = 'BIOS/UEFI' + event['component_type'] = ipmiconst.sensor_type_codes[0xf] + status = oemdata[0] + method = (status & 0b11110000) >> 4 + status = (status & 0b1111) + if method == 1: + event['event'] = 'Automatic recovery' + elif method == 2: + event['event'] = 'Manual recovery' + if status == 0: + event['event'] += '- Failed' + event['severity'] = pygconst.Health.Failed + if oemdata[1] == 0x1: + event['event'] += '- BIOS recovery image not found' + event['event_data'] = '{0}.{1}'.format(oemdata[2], oemdata[3]) + elif oemtype == 0xd2: # eMMC status + if oemdata[0] == 1: + event['component'] = 'eMMC' + event['component_type'] = ipmiconst.sensor_type_codes[0xc] + if oemdata[0] == 1: + event['event'] = 'eMMC Format error' + event['severity'] = pygconst.Health.Failed + elif oemtype == 0xd3: + if oemdata[0] == 1: + event['event'] = 'User privilege modification' + event['severity'] = pygconst.Health.Ok + event['component'] = 'User Privilege' + event['component_type'] = ipmiconst.sensor_type_codes[6] + event['event_data'] = ( + 'User {0} on channel {1} had privilege changed ' + 'from {2} to {3}'.format( + oemdata[2], oemdata[1], oemdata[3] & 0b1111, + (oemdata[3] & 0b11110000) >> 4) + ) + else: + event['event'] = 'OEM event: {0}'.format( + ' '.join(format(x, '02x') for x in event['oemdata'])) + del event['oemdata'] + return + evdata = event['event_data_bytes'] + if event['event_type_byte'] == 0x75: # ME event + event['component'] = 'ME Firmware' + event['component_type'] = ipmiconst.sensor_type_codes[0xf] + event['event'], event['severity'] = me_status.get( + evdata[1], ('Unknown', pygconst.Health.Warning)) + if evdata[1] == 3: + event['event'], event['severity'] = me_flash_status.get( + evdata[2], ('Unknown state', pygconst.Health.Warning)) + elif evdata[1] == 9: + event['event'] += ' (0x{0:2x})'.format(evdata[2]) + elif evdata[1] == 0xf and evdata[2] & 0b10000000: + event['event'] = 'Auto configuration failed' + event['severity'] = pygconst.Health.Critical + # For HDD bay events, the event data 2 is the bay, modify + # the description to be more specific + if (event['event_type_byte'] == 0x6f + and (evdata[0] & 0b11000000) == 0b10000000 + and event['component_type_id'] == 13): + event['component'] += ' {0}'.format(evdata[1] & 0b11111) + + async def reseat_bay(self, bay): + if self.is_fpc: + return await self.smmhandler.reseat_bay(bay) + elif self.has_xcc and bay == -1: + return await self.immhandler.reseat() + return await super(OEMHandler, self).reseat_bay(bay) + + async def get_ntp_enabled(self): + if await self.has_tsm() or await self.has_ami() or await self.has_asrock(): + ntpres = await self.ipmicmd.raw_command(netfn=0x32, command=0xa7) + return ntpres['data'][0] == '\x01' + elif await self.is_fpc(): + return await self.smmhandler.get_ntp_enabled(self._fpc_variant) + elif self.has_tsma: + return self.tsmahandler.get_ntp_enabled() + return None + + async def get_ntp_servers(self): + if await self.has_tsm() or await self.has_ami() or await self.has_asrock(): + srvs = [] + ntpres = await self.ipmicmd.raw_command(netfn=0x32, command=0xa7) + srvs.append(ntpres['data'][1:129].rstrip('\x00')) + srvs.append(ntpres['data'][129:257].rstrip('\x00')) + return srvs + if await self.is_fpc(): + return await self.smmhandler.get_ntp_servers() + if self.has_tsma: + return self.tsmahandler.get_ntp_servers() + return () + + async def set_ntp_enabled(self, enabled): + if await self.has_tsm() or await self.has_ami() or await self.has_asrock(): + if enabled: + await self.ipmicmd.raw_command( + netfn=0x32, command=0xa8, data=(3, 1), timeout=15) + else: + await self.ipmicmd.raw_command( + netfn=0x32, command=0xa8, data=(3, 0), timeout=15) + return True + if await self.is_fpc(): + await self.smmhandler.set_ntp_enabled(enabled) + return True + if self.has_tsma: + await self.tsmahandler.set_ntp_enabled(enabled) + return None + + async def set_ntp_server(self, server, index=0): + if await self.has_tsm() or await self.has_ami() or await self.has_asrock(): + if not (0 <= index <= 1): + raise pygexc.InvalidParameterValue("Index must be 0 or 1") + cmddata = bytearray((1 + index, )) + cmddata += server.ljust(128, '\x00') + await self.ipmicmd.raw_command(netfn=0x32, command=0xa8, data=cmddata) + return True + elif await self.is_fpc(): + if not 0 <= index <= 2: + raise pygexc.InvalidParameterValue( + 'SMM supports indexes 0 through 2') + await self.smmhandler.set_ntp_server(server, index) + return True + elif self.has_tsma: + if not (0 <= index <= 1): + raise pygexc.InvalidParameterValue("Index must be 0 or 1") + return await self.tsmahandler.set_ntp_server(server, index) + return None + + async def user_delete(self, uid, channel): + if await self.has_xcc(): + return await self.immhandler.user_delete(uid) + + async def set_user_access(self, uid, channel, callback, link_auth, ipmi_msg, + privilege_level): + if await self.is_fpc() and self._fpc_variant != 6: + await self.smmhandler.set_user_priv(uid, privilege_level) + + async def is_fpc(self): + """True if the target is a Lenovo nextscale fan power controller""" + + if self._fpc_variant is not None: + return self._fpc_variant + has_imm = await self.has_imm() + has_xcc = await self.has_xcc() + if has_imm or has_xcc: + return None + fpc_ids = ((19046, 32, 1063), (20301, 32, 462)) + smm_id = (19046, 32, 1180) + smmv2_2u = (19046, 32, 1360) + smmv2_6u = (19046, 32, 1387) + currid = (self.oemid['manufacturer_id'], self.oemid['device_id'], + self.oemid['product_id']) + if currid in fpc_ids: + self._fpc_variant = 6 + elif currid == smm_id: + self._fpc_variant = 2 + elif currid == smmv2_2u: + self._fpc_variant = 0x22 + elif currid == smmv2_6u: + self._fpc_variant = 0x26 + return self._fpc_variant + + @property + def is_sd350(self): + return (19046, 32, 13616) == (self.oemid['manufacturer_id'], + self.oemid['device_id'], + self.oemid['product_id']) + + tsma_ids = ((19046, 32, 1287),) + + @property + def has_tsma(self): + currid = (self.oemid['manufacturer_id'], self.oemid['device_id'], + self.oemid['product_id']) + return currid in self.tsma_ids + + async def has_tsm(self): + """True if this particular server have a TSM based service processor""" + + if (self.oemid['manufacturer_id'] == 19046 + and self.oemid['device_id'] == 32): + try: + await self.ipmicmd.raw_command(netfn=0x3a, command=0xf) + except pygexc.IpmiException as ie: + if ie.ipmicode == 193: + return False + raise + return True + return False + + async def has_asrock(self): + # True if this particular server have a ASROCKRACK + # based service processor (RS160 or TS460) + # RS160 (Riddler) product id is 1182 (049Eh) + # TS460 (WildThing) product id is 1184 (04A0h) + + if (self.oemid['manufacturer_id'] == 19046 + and (self.oemid['product_id'] == 1182 + or self.oemid['product_id'] == 1184)): + try: + await self.ipmicmd.raw_command(netfn=0x3a, + command=0x50, + data=(0x00, 0x00, 0x00)) + except pygexc.IpmiException as ie: + if ie.ipmicode == 193: + return False + raise + return True + return False + + @property + def isTS460(self): + if self.oemid['product_id'] == 1184: + return True + return False + + async def get_oem_inventory_descriptions(self): + if await self.has_tsm() or await self.has_ami() or await self.has_asrock(): + # Thinkserver with TSM + if not self.oem_inventory_info: + async for desc in self._collect_tsm_inventory(): + yield desc + for compname in self.oem_inventory_info: + yield compname + elif await self.has_imm(): + async for desc in self.immhandler.get_hw_descriptions(): + yield desc + elif await self.is_fpc(): + async for desc in self.smmhandler.get_inventory_descriptions(self.ipmicmd, + await self.is_fpc()): + yield desc + + async def get_oem_inventory(self): + if await self.has_tsm() or await self.has_ami() or await self.has_asrock(): + await self._collect_tsm_inventory() + for compname in self.oem_inventory_info: + yield (compname, self.oem_inventory_info[compname]) + elif await self.has_imm(): + async for inv in self.immhandler.get_hw_inventory(): + yield inv + elif await self.is_fpc(): + async for compname in self.smmhandler.get_inventory_descriptions( + self.ipmicmd, await self.is_fpc()): + yield (compname, self.smmhandler.get_inventory_of_component( + self.ipmicmd, compname)) + + async def get_sensor_data(self): + if await self.has_imm(): + async for name in self.immhandler.get_oem_sensor_names(self.ipmicmd): + yield self.immhandler.get_oem_sensor_reading(name, + self.ipmicmd) + elif await self.is_fpc(): + for name in nextscale.get_sensor_names(self.ipmicmd, + self._fpc_variant): + yield nextscale.get_sensor_reading(name, self.ipmicmd, + self._fpc_variant) + elif await self.has_ami(): + self.get_ami_sensor_data() + + async def get_sensor_descriptions(self): + if await self.has_imm(): + async for desc in self.immhandler.get_oem_sensor_descriptions(self.ipmicmd): + yield desc + elif await self.is_fpc(): + async for desc in nextscale.get_sensor_descriptions( + self.ipmicmd, self._fpc_variant): + yield desc + elif await self.has_ami(): + async for desc in self.get_ami_sensor_descriptions(): + yield desc + if False: + yield None + + async def get_sensor_reading(self, sensorname): + if await self.has_imm(): + return self.immhandler.get_oem_sensor_reading(sensorname, + self.ipmicmd) + elif await self.is_fpc(): + return nextscale.get_sensor_reading(sensorname, self.ipmicmd, + self._fpc_variant) + elif await self.has_ami(): + self.get_ami_sensor_reading(sensorname) + return () + + async def get_inventory_of_component(self, component): + if await self.has_tsm() or await self.has_ami() or await self.has_asrock(): + await self._collect_tsm_inventory() + return await self.oem_inventory_info.get(component, None) + if await self.has_imm(): + return await self.immhandler.get_component_inventory(component) + if await self.is_fpc(): + return await self.smmhandler.get_inventory_of_component(component) + + def get_cmd_type(self, categorie_item, catspec): + if self.has_asrock: + cmd_type = catspec["command"]["asrock"] + elif categorie_item in categorie_items: + cmd_type = catspec["command"]["lenovo"] + else: + cmd_type = catspec["command"] + + return cmd_type + + async def _collect_tsm_inventory(self): + self.oem_inventory_info = {} + asrock = False + if self.has_asrock: + asrock = True + for catid, catspec in inventory.categories.items(): + # skip the inventory fields if the system is RS160 + if asrock and catid not in categorie_items: + continue + if (catspec.get("workaround_bmc_bug", False) + and catspec["workaround_bmc_bug"]( + "ami" if await self.has_ami() else "lenovo")): + rsp = None + cmd = self.get_cmd_type(catid, catspec) + tmp_command = dict(cmd) + tmp_command["data"] = list(tmp_command["data"]) + count = 0 + for i in range(0x01, 0xff): + tmp_command["data"][-1] = i + try: + partrsp = await self.ipmicmd.raw_command(**tmp_command) + count += 1 + if asrock and partrsp["data"][1] == "\xff": + continue + + if rsp is None: + rsp = partrsp + rsp["data"] = list(rsp["data"]) + else: + rsp["data"].extend(partrsp["data"][1:]) + except Exception: + break + # If we didn't get any response, assume we don't have + # this category and go on to the next one + if rsp is None: + continue + rsp["data"].insert(1, count) + rsp["data"] = buffer(bytearray(rsp["data"])) + else: + try: + cmd = self.get_cmd_type(catid, catspec) + rsp = await self.ipmicmd.raw_command(**cmd) + except pygexc.IpmiException: + continue + # Parse the response we got + try: + items = inventory.parse_inventory_category( + catid, rsp, asrock, + countable=catspec.get("countable", True) + ) + except Exception: + # If we can't parse an inventory category, ignore it + print(traceback.print_exc()) + continue + + for item in items: + try: + # Originally on ThinkServer and SD350 (Kent), + # the DIMM is distinguished by slot, + # the key is the value of slot number (item["index"]) + # While on RS160/TS460 the DIMMs is distinguished + # by slot number and channel number, + # the key is the value of the sum of slot number + # and channel number + if asrock and catid == "dimm": + if item["channel_number"] == 1: + key = catspec["idstr"].format(item["index"]) + else: + key = catspec["idstr"].format( + item["index"] + item["channel_number"]) + else: + key = catspec["idstr"].format(item["index"]) + + del item["index"] + self.oem_inventory_info[key] = item + except Exception: + # If we can't parse an inventory item, ignore it + print(traceback.print_exc()) + continue + + async def get_leds(self): + cmd = 0x02 + led_set = leds + led_set_status = led_status + + asrock = await self.has_asrock() + if await self.has_ami(): + cmd = 0x05 + led_set = ami_leds + led_set_status = ami_led_status + elif asrock: + cmd = 0x50 + # because rs160 has different led info with ts460 + if self.isTS460: + led_set = ts460_leds + else: + led_set = asrock_leds + led_set_status = asrock_led_status + + for (name, id_) in led_set.items(): + try: + if asrock: + rsp = await self.ipmicmd.raw_command(netfn=0x3A, command=cmd, + data=(0x03, id_, 0x00)) + rdata = bytearray(rsp['data'][:]) + status = rdata[1] + else: + rsp = await self.ipmicmd.raw_command(netfn=0x3A, command=cmd, + data=(id_,)) + rdata = bytearray(rsp['data'][:]) + status = rdata[0] + + except pygexc.IpmiException: + continue # Ignore LEDs we can't retrieve + status = led_set_status.get(status, + led_status_default) + yield (name, {'status': status}) + + async def set_identify(self, on, duration, blink): + if on and not duration and self.is_sd350: + await self.ipmicmd.raw_command(netfn=0x3a, command=6, data=(1, 1)) + elif await self.has_xcc(): + await self.immhandler.set_identify(on, duration, blink) + else: + raise pygexc.UnsupportedFunctionality() + + async def process_fru(self, fru, name=None): + if fru is None: + return fru + if await self.has_tsm(): + fru['oem_parser'] = 'lenovo' + # Thinkserver lays out specific interpretation of the + # board extra fields + try: + _, _, wwn1, wwn2, mac1, mac2 = fru['board_extra'] + if wwn1 not in ('0000000000000000', ''): + fru['WWN 1'] = wwn1.encode('utf-8') + if wwn2 not in ('0000000000000000', ''): + fru['WWN 2'] = wwn2.encode('utf-8') + if mac1 not in ('00:00:00:00:00:00', ''): + fru['MAC Address 1'] = mac1.encode('utf-8') + if mac2 not in ('00:00:00:00:00:00', ''): + fru['MAC Address 2'] = mac2.encode('utf-8') + except (AttributeError, KeyError): + pass + try: + # The product_extra field is UUID as the system would present + # in DMI. This is different than the two UUIDs that + # it returns for get device and get system uuid... + byteguid = fru['product_extra'][0] + # It can present itself as claiming to be ASCII when it + # is actually raw hex. As a result it triggers the mechanism + # to strip \x00 from the end of text strings. Work around this + # by padding with \x00 to the right if less than 16 long + byteguid.extend('\x00' * (16 - len(byteguid))) + if byteguid not in ('\x20' * 16, '\x00' * 16, '\xff' * 16): + fru['UUID'] = util.decode_wireformat_uuid(byteguid) + except (AttributeError, KeyError, IndexError): + pass + return fru + elif await self.has_imm(): + fru['oem_parser'] = 'lenovo' + try: + bextra = fru['board_extra'] + fru['FRU Number'] = bextra[0] + fru['Revision'] = bextra[4] + macs = bextra[6] + if macs: + macprefix = None + idx = 0 + endidx = len(macs) - 5 + macprefix = None + while idx < endidx: + currmac = macs[idx:idx + 6] + if not isinstance(currmac, bytearray): + # invalid vpd format, abort attempts to extract + # mac in this way + break + if currmac == b'\x00\x00\x00\x00\x00\x00': + break + # VPD may veer off, detect and break off + if macprefix is None: + macprefix = currmac[:3] + elif currmac[:3] != macprefix: + break + ms = mac_format.format(*currmac) + ifidx = idx / 6 + 1 + fru['MAC Address {0}'.format(ifidx)] = ms + idx = idx + 6 + except (AttributeError, KeyError, IndexError): + pass + if await self.has_xcc() and name and name.startswith('PSU '): + self.immhandler.augment_psu_info(fru, name) + if (await self.has_xcc() and 'memory_type' in fru + and fru['memory_type'] == 'Unknown'): + await self.immhandler.fetch_dimm(name, fru) + return fru + elif await self.is_fpc() and await self.is_fpc() != 6: # SMM variant + fru['oem_parser'] = 'lenovo' + return await self.smmhandler.process_fru(fru) + elif await self.has_asrock(): + fru['oem_parser'] = 'lenovo' + # ASRock RS160 TS460 lays out specific interpretation of the + # board extra fields + try: + mac1 = fru['board_extra'] + if mac1 not in ('00:00:00:00:00:00', ''): + fru['MAC Address 1'] = mac1.encode('utf-8') + except (AttributeError, KeyError): + pass + return fru + else: + fru['oem_parser'] = None + return fru + + async def has_xcc(self): + if self._hasxcc is not None: + return self._hasxcc + try: + bdata = await self.ipmicmd.raw_command(netfn=0x3a, command=0xc1) + except pygexc.IpmiException: + self._hasxcc = False + self._hasimm = False + return False + if len(bdata['data'][:]) != 3: + self._hasimm = False + self._hasxcc = False + return False + rdata = bytearray(bdata['data'][:]) + self._hasxcc = rdata[1] & 16 == 16 + if self._hasxcc: + # For now, have imm calls go to xcc, since they are providing same + # interface. Longer term the hope is that all the Lenovo + # stuff will branch at init, and not have conditionals + # in all the functions + self._hasimm = self._hasxcc + return self._hasxcc + + async def has_imm(self): + if self._hasimm is not None: + return self._hasimm + try: + bdata = await self.ipmicmd.raw_command(netfn=0x3a, command=0xc1) + except pygexc.IpmiException: + self._hasimm = False + return False + if len(bdata['data'][:]) != 3: + self._hasimm = False + return False + rdata = bytearray(bdata['data'][:]) + self._hasimm = (rdata[1] & 1 == 1) or (rdata[1] & 16 == 16) + return self._hasimm + + async def has_ami(self): + """True if this particular server is AMI based lenovo server + + """ + if(self.oemid['manufacturer_id'] == 19046 + and self.oemid['product_id'] == 13616): + try: + rsp = await self.ipmicmd.raw_command(netfn=0x3a, command=0x80) + except pygexc.IpmiException as ie: + if ie.ipmicode == 193: + return False + raise + rdata = bytearray(rsp['data'][:]) + if rdata[0] in range(5): + return True + else: + return False + return False + + async def get_oem_firmware(self, bmcver, components, category): + if await self.has_tsm() or await self.has_ami() or await self.has_asrock(): + command = firmware.get_categories()["firmware"] + fw_cmd = self.get_cmd_type("firmware", command) + rsp = await self.ipmicmd.raw_command(**fw_cmd) + + # the newest Lenovo ThinkServer versions are returning Bios version + # numbers through another command + bios_versions = None + if await self.has_tsm() or await self.has_asrock(): + bios_command = firmware.get_categories()["bios_version"] + bios_cmd = self.get_cmd_type("bios_version", bios_command) + bios_rsp = await self.ipmicmd.raw_command(**bios_cmd) + if await self.has_asrock(): + bios_versions = bios_rsp['data'] + else: + bios_versions = bios_command["parser"](bios_rsp['data']) + # pass bios versions to firmware parser + for x in command["parser"](rsp["data"], + bios_versions, + await self.has_asrock()): + yield x + elif await self.has_imm(): + async for x in self.immhandler.get_firmware_inventory(bmcver, components, category): + yield x + elif await self.is_fpc(): + async for x in nextscale.get_fpc_firmware(bmcver, self.ipmicmd, + self._fpc_variant): + yield x + elif self.has_tsma: + async for x in self.tsmahandler.get_firmware_inventory( + components, raisebypass=False, ipmicmd=self.ipmicmd): + yield x + async for x in super(OEMHandler, self).get_oem_firmware(bmcver, components, category): + yield x + + async def get_diagnostic_data(self, savefile, progress, autosuffix=False): + if await self.has_xcc(): + return await self.immhandler.get_diagnostic_data(savefile, progress, + autosuffix) + if await self.is_fpc(): + return await self.smmhandler.get_diagnostic_data(savefile, progress, + autosuffix, + self._fpc_variant) + if self.has_tsma: + return await self.tsmahandler.get_diagnostic_data(savefile, progress, + autosuffix) + + async def get_oem_capping_enabled(self): + if await self.has_tsm(): + rsp = await self.ipmicmd.raw_command(netfn=0x3a, command=0x1b, + data=(3,)) + # disabled + if rsp['data'][0] == '\x00': + return False + # enabled + else: + return True + + async def set_oem_capping_enabled(self, enable): + """Set PSU based power capping + + :param enable: True for enable and False for disable + """ + # 1 - Enable power capping(default) + if enable: + statecode = 1 + # 0 - Disable power capping + else: + statecode = 0 + if await self.has_tsm(): + await self.ipmicmd.raw_command(netfn=0x3a, command=0x1a, + data=(3, statecode)) + return True + + async def get_oem_remote_kvm_available(self): + if await self.has_tsm(): + rsp = await self.ipmicmd.raw_command(netfn=0x3a, command=0x13) + return rsp['data'][0] == 0 + return False + + async def _restart_dns(self): + if await self.has_tsm(): + await self.ipmicmd.raw_command(netfn=0x32, command=0x6c, data=(7, 0)) + + async def get_oem_domain_name(self): + if await self.has_tsm(): + name = '' + for i in range(1, 5): + rsp = await self.ipmicmd.raw_command(netfn=0x32, command=0x6b, + data=(4, i)) + name += rsp['data'][:] + return name.rstrip('\x00') + elif await self.is_fpc(): + return await self.smmhandler.get_domain() + + async def set_oem_domain_name(self, name): + if await self.has_tsm(): + # set the domain name length + data = [3, 0, 0, 0, 0, len(name)] + await self.ipmicmd.raw_command(netfn=0x32, command=0x6c, data=data) + + # set the domain name content + name = name.ljust(256, "\x00") + for i in range(0, 4): + data = [4, i + 1] + offset = i * 64 + data.extend([ord(x) for x in name[offset:offset + 64]]) + await self.ipmicmd.raw_command(netfn=0x32, command=0x6c, data=data) + + await self._restart_dns() + return + elif await self.is_fpc(): + await self.smmhandler.set_domain(name) + + async def set_hostname(self, hostname): + if await self.has_xcc(): + return await self.immhandler.set_hostname(hostname) + elif await self.is_fpc(): + return await self.smmhandler.set_hostname(hostname) + return await super(OEMHandler, self).set_hostname(hostname) + + async def get_hostname(self): + if await self.has_xcc(): + return await self.immhandler.get_hostname() + elif await self.is_fpc(): + return await self.smmhandler.get_hostname() + return await super(OEMHandler, self).get_hostname() + + """ Gets a remote console launcher for a Lenovo ThinkServer. + + Returns a tuple: (content type, launcher) or None if the launcher could + not be retrieved.""" + async def _get_ts_remote_console(self, bmc, username, password): + # We don't establish non-secure connections without checking + # certificates + if not self.ipmicmd.certverify: + return + conn = wc.WebConnection(bmc, 443, + verifycallback=self.ipmicmd.certverify) + conn.connect() + params = urlencode({ + 'WEBVAR_USERNAME': username, + 'WEBVAR_PASSWORD': password + }) + headers = { + 'Connection': 'keep-alive' + } + rsp = await conn.grab_response_with_status('/rpc/WEBSES/create.asp', params, headers=headers) + + if rsp[1] == 200: + conn.cookies = {} + body = rsp[0].decode().split('\n') + session_line = None + for line in body: + if 'SESSION_COOKIE' in line: + session_line = line + if session_line is None: + return + + session_id = session_line.split('\'')[3] + # Usually happens when maximum number of sessions is reached + if session_id == 'Failure_Session_Creation': + return + + headers = { + 'Connection': 'keep-alive', + 'Cookie': 'SessionCookie=' + session_id, + } + rsp = await conn.grab_response_with_status('/Java/jviewer.jnlp?EXTRNIP=' + bmc + '&JNLPSTR=JViewer', headers=headers) + if rsp[1] == 200: + return rsp[2]['Content-Type'], base64.b64encode( + rsp[0]) + + async def get_graphical_console(self): + return await self._get_ts_remote_console(self.ipmicmd.bmc, + self.ipmicmd.ipmi_session.userid, + self.ipmicmd.ipmi_session.password) + + async def add_extra_net_configuration(self, netdata, channel=None): + if await self.has_tsm(): + ipv6_addr = await self.ipmicmd.raw_command( + netfn=0x0c, command=0x02, + data=(0x01, 0xc5, 0x00, 0x00))["data"][1:] + if not ipv6_addr: + return + rspdata = await self.ipmicmd.raw_command( + netfn=0xc, command=0x02, + data=(0x1, 0xc6, 0, 0))['data'] + ipv6_prefix_ba = bytearray(rspdata) + ipv6_prefix = ipv6_prefix_ba[1] + + if hasattr(socket, 'inet_ntop'): + ipv6str = socket.inet_ntop(socket.AF_INET6, ipv6_addr) + else: + # fall back to a dumber, but more universal formatter + ipv6str = binascii.b2a_hex(ipv6_addr) + ipv6str = ':'.join([ipv6str[x:x + 4] for x in range(0, 32, 4)]) + netdata['ipv6_addresses'] = [ + '{0}/{1}'.format(ipv6str, ipv6_prefix)] + + async def has_megarac(self): + # if there is functionality that is the same for tsm or generic + # megarac, then this is appropriate. If there's a TSM specific + # preferred, use has_tsm first + if self._has_megarac is not None: + return self._has_megarac + self._has_megarac = False + try: + rsp = await self.ipmicmd.raw_command(netfn=0x32, command=0x7e) + # We don't have a handy classify-only, so use get sel policy + # rsp should have a length of one, and be either '\x00' or '\x01' + if len(rsp['data'][:]) == 1 and rsp['data'][0] in ('\x00', '\x01'): + self._has_megarac = True + except pygexc.IpmiException as ie: + if ie.ipmicode == 0: + # if it's a generic IpmiException rather than an error code + # from the BMC, then this is a deeper problem than just an + # invalid command or command length or similar + raise + return self._has_megarac + + async def set_alert_ipv6_destination(self, ip, destination, channel): + if await self.has_megarac(): + ethidx = await self._megarac_eth_index() + reqdata = bytearray([channel, 193, destination, ethidx, 0]) + parsedip = socket.inet_pton(socket.AF_INET6, ip) + reqdata.extend(parsedip) + reqdata.extend('\x00\x00\x00\x00\x00\x00') + await self.ipmicmd.raw_command(netfn=0xc, command=1, data=reqdata) + return True + return False + + async def _set_short_ris_string(self, selector, value): + data = (1, selector, 0) + struct.unpack('{0}B'.format(len(value)), + value) + await self.ipmicmd.raw_command(netfn=0x32, command=0x9f, data=data) + + async def _set_ris_string(self, selector, value): + if len(value) > 256: + raise pygexc.UnsupportedFunctionality( + 'Value exceeds 256 characters: {0}'.format(value)) + padded = value + (256 - len(value)) * '\x00' + padded = list(struct.unpack('256B', padded)) + # 8 = RIS, 4 = hd, 2 = fd, 1 = cd + try: # try and clear in-progress if left incomplete + await self.ipmicmd.raw_command(netfn=0x32, command=0x9f, + data=(1, selector, 0, 0)) + except pygexc.IpmiException: + pass + # set in-progress + await self.ipmicmd.raw_command(netfn=0x32, command=0x9f, + data=(1, selector, 0, 1)) + # now do the set + for x in range(0, 256, 64): + currdata = padded[x:x + 64] + currchunk = x // 64 + 1 + cmddata = [1, selector, currchunk] + currdata + await self.ipmicmd.raw_command(netfn=0x32, command=0x9f, data=cmddata) + # unset in-progress + await self.ipmicmd.raw_command(netfn=0x32, command=0x9f, + data=(1, selector, 0, 0)) + + async def _megarac_fetch_image_shortnames(self): + rsp = await self.ipmicmd.raw_command(netfn=0x32, command=0xd8, + data=(7, 1, 0)) + imgnames = rsp['data'][1:] + shortnames = [] + for idx in range(0, len(imgnames), 22): + shortnames.append(imgnames[idx + 2:idx + 22].rstrip('\0')) + return shortnames + + async def _megarac_media_waitforready(self, imagename): + # first, we have, sadly, a 10 second grace period for some invisible + # async activity to get far enough long to monitor + await self.ipmicmd.ipmi_session.pause(10) + risenabled = '\x00' + mountok = '\xff' + while risenabled != '\x01': + risenabled = (await self.ipmicmd.raw_command( + netfn=0x32, command=0x9e, data=(8, 10)))['data'][2] + while mountok == '\xff': + mountok = (await self.ipmicmd.raw_command( + netfn=0x32, command=0x9e, data=(1, 8)))['data'][2] + targshortname = _megarac_abbrev_image(imagename) + shortnames = await self._megarac_fetch_image_shortnames() + while targshortname not in shortnames: + self.ipmicmd.wait_for_rsp(1) + shortnames = await self._megarac_fetch_image_shortnames() + self.ipmicmd.ipmi_session.pause(10) + try: + await self.ipmicmd.raw_command(netfn=0x32, command=0xa0, data=(1, 0)) + await self.ipmicmd.ipmi_session.pause(5) + except pygexc.IpmiException: + pass + + async def _megarac_attach_media(self, proto, username, password, imagename, + domain, path, host): + # First we must ensure that the RIS is actually enabled + await self.ipmicmd.raw_command(netfn=0x32, command=0x9f, data=(8, 10, 0, 1)) + if username is not None: + await self._set_ris_string(3, username) + if password is not None: + await self._set_short_ris_string(4, password) + if domain is not None: + await self._set_ris_string(6, domain) + await self._set_ris_string(1, path) + ip = util.get_ipv4(host)[0] + await self._set_short_ris_string(2, ip) + await self._set_short_ris_string(5, proto) + # now to restart RIS to have changes take effect... + await self.ipmicmd.raw_command(netfn=0x32, command=0x9f, data=(8, 11)) + # now to kick off the requested mount + await self._megarac_media_waitforready(imagename) + await self._set_ris_string(0, imagename) + await self.ipmicmd.raw_command(netfn=0x32, command=0xa0, + data=(1, 1)) + + async def attach_remote_media(self, url, username, password): + if await self.has_imm(): + await self.immhandler.attach_remote_media(url, username, password) + elif self.has_tsma: + return await self.tsmahandler.attach_remote_media( + url, username, password, None) + elif await self.has_megarac(): + proto, host, path = util.urlsplit(url) + if proto == 'smb': + proto = 'cifs' + domain = None + path, imagename = path.rsplit('/', 1) + if username is not None and '@' in username: + username, domain = username.split('@', 1) + elif username is not None and '\\' in username: + domain, username = username.split('\\', 1) + try: + await self._megarac_attach_media(proto, username, password, + imagename, domain, path, host) + except pygexc.IpmiException as ie: + if ie.ipmicode in (0x92, 0x99): + # if starting from scratch, this can happen... + await self._megarac_attach_media(proto, username, password, + imagename, domain, path, host) + else: + raise + + async def get_update_status(self): + if await self.is_fpc() or self.has_tsma: + return "ready" + if await self.has_xcc(): + return await self.immhandler.get_update_status() + return await super(OEMHandler, self).get_update_status() + + async def update_firmware(self, filename, data=None, progress=None, bank=None): + if await self.has_xcc(): + return await self.immhandler.update_firmware( + filename, data=data, progress=progress, bank=bank) + if await self.is_fpc(): + return await self.smmhandler.update_firmware( + filename, data=data, progress=progress, bank=bank) + if self.has_tsma: + return await self.tsmahandler.update_firmware( + filename, data=data, progress=progress, bank=bank) + return await super(OEMHandler, self).update_firmware(filename, data=data, + progress=progress, + bank=bank) + + async def get_description(self): + if await self.has_xcc(): + return await self.immhandler.get_description() + if await self.is_fpc(): + return {'height': self._fpc_variant & 0xf, 'slot': 0} + return await super(OEMHandler, self).get_description() + + async def get_extended_bmc_configuration(self): + if await self.has_xcc(): + return await self.immhandler.get_extended_bmc_configuration() + return await super(OEMHandler, self).get_extended_bmc_configuration() + + async def get_bmc_configuration(self): + if await self.has_xcc(): + return await self.immhandler.get_bmc_configuration() + if await self.is_fpc(): + return await self.smmhandler.get_bmc_configuration(self._fpc_variant) + if self.has_tsma: + return await self.tsmahandler.get_bmc_configuration() + return await super(OEMHandler, self).get_bmc_configuration() + + async def set_bmc_configuration(self, changeset): + if await self.has_xcc(): + return await self.immhandler.set_bmc_configuration(changeset) + if await self.is_fpc(): + return await self.smmhandler.set_bmc_configuration( + changeset, self._fpc_variant) + if self.has_tsma: + return await self.tsmahandler.set_bmc_configuration( + changeset) + return await super(OEMHandler, self).set_bmc_configuration(changeset) + + async def get_system_configuration(self, hideadvanced): + if await self.has_imm() or await self.has_xcc(): + return await self.immhandler.get_system_configuration(hideadvanced) + if self.has_tsma: + return await self.tsmahandler.get_uefi_configuration(hideadvanced) + return await super(OEMHandler, self).get_system_configuration(hideadvanced) + + async def set_system_configuration(self, changeset): + if await self.has_imm() or await self.has_xcc(): + return await self.immhandler.set_system_configuration(changeset) + if self.has_tsma: + return await self.tsmahandler.set_uefi_configuration(changeset) + return await super(OEMHandler, self).set_system_configuration(changeset) + + async def clear_bmc_configuration(self): + if await self.has_xcc(): + return await self.immhandler.clear_bmc_configuration() + elif await self.is_fpc(): + return await self.smmhandler.clear_bmc_configuration() + elif self.has_tsma: + return await self.tsmahandler.clear_bmc_configuration() + return await super(OEMHandler, self).clear_system_configuration() + + async def clear_system_configuration(self): + if await self.has_xcc(): + return await self.immhandler.clear_system_configuration() + if self.has_tsma: + return await self.tsmahandler.clear_uefi_configuration() + return await super(OEMHandler, self).clear_system_configuration() + + async def detach_remote_media(self): + if await self.has_imm(): + await self.immhandler.detach_remote_media() + elif self.has_tsma: + await self.tsmahandler.detach_remote_media() + elif await self.has_megarac(): + await self.ipmicmd.raw_command( + netfn=0x32, command=0x9f, data=(8, 10, 0, 0)) + await self.ipmicmd.raw_command(netfn=0x32, command=0x9f, data=(8, 11)) + + async def upload_media(self, filename, progress, data): + if await self.has_xcc() or await self.has_imm(): + return await self.immhandler.upload_media(filename, progress, data) + return await super(OEMHandler, self).upload_media(filename, progress, data) + + async def list_media(self): + if await self.has_xcc() or await self.has_imm(): + async for x in self.immhandler.list_media(): + yield x + return + if self.has_tsma: + async for x in self.tsmahandler.list_media(): + yield x + return + async for x in super(OEMHandler, self).list_media(): + yield x + + async def get_health(self, summary): + if await self.has_xcc(): + return await self.immhandler.get_health(summary) + return await super(OEMHandler, self).get_health(summary) + + async def get_licenses(self): + if await self.has_xcc(): + async for x in self.immhandler.get_licenses(): + yield x + return + async for x in super(OEMHandler, self).get_licenses(): + yield x + + async def get_user_expiration(self, uid): + if await self.has_xcc(): + return await self.immhandler.get_user_expiration(uid) + return None + + async def delete_license(self, name): + if await self.has_xcc(): + return await self.immhandler.delete_license(name) + return await super(OEMHandler, self).delete_license(name) + + async def save_licenses(self, directory): + if await self.has_xcc(): + async for x in self.immhandler.save_licenses(directory): + yield x + return + async for x in super(OEMHandler, self).save_licenses(directory): + yield x + + async def apply_license(self, filename, progress=None, data=None): + if await self.has_xcc(): + async for x in self.immhandler.apply_license(filename, progress, data): + yield x + return + async for x in super(OEMHandler, self).apply_license(filename, progress, data): + yield x + + async def set_oem_extended_privilleges(self, uid): + """Set user extended privillege as 'KVM & VMedia Allowed' + + |KVM & VMedia Not Allowed 0x00 0x00 0x00 0x00 + |KVM Only Allowed 0x01 0x00 0x00 0x00 + |VMedia Only Allowed 0x02 0x00 0x00 0x00 + |KVM & VMedia Allowed 0x03 0x00 0x00 0x00 + + :param uid: User ID. + """ + if await self.has_tsm(): + await self.ipmicmd.raw_command(netfn=0x32, command=0xa3, data=( + uid, 0x03, 0x00, 0x00, 0x00)) + return True + return False + + async def get_user_privilege_level(self, uid): + if await self.has_xcc(): + return await self.immhandler.get_user_privilege_level(uid) + return None + + async def set_user_access(self, uid, channel, callback, link_auth, ipmi_msg, privilege_level): + if await self.has_xcc(): + await self.immhandler.set_user_access(uid, privilege_level) + + async def process_zero_fru(self, zerofru): + if (self.oemid['manufacturer_id'] == 19046 + and self.oemid['product_id'] == 13616): + # Currently SD350 FRU UUID is synchronized with the Device UUID. + # Need to change to System UUID in future. + # Since the IPMI get device uuid matches SMBIOS, + # no need to decode it. + guiddata = await self.ipmicmd.raw_command(netfn=6, command=0x8) + if 'error' not in guiddata: + zerofru['UUID'] = util.decode_wireformat_uuid( + guiddata['data'], True) + else: + # It is expected that a manufacturer matches SMBIOS to IPMI + # get system uuid return data. If a manufacturer does not + # do so, they should handle either deletion or fixup in the + # OEM processing pass. Code optimistically assumes that if + # data is returned, than the vendor is properly using it. + + guiddata = await self.ipmicmd.raw_command(netfn=6, command=0x37) + if 'error' not in guiddata: + if (self.oemid['manufacturer_id'] == 19046 + and (self.oemid['product_id'] == 1182 + or self.oemid['product_id'] == 1184)): + # The manufacturer (Asrockrack) of RS160/TS460 + # matches SMBIOS + # to IPMI get system uuid return data, + # no need to decode it. + zerofru['UUID'] = util.decode_wireformat_uuid( + guiddata['data'], True) + else: + zerofru['UUID'] = util.decode_wireformat_uuid( + guiddata['data']) + if await self.is_fpc(): + await self.smmhandler.augment_zerofru(zerofru, self._fpc_variant) + return await self.process_fru(zerofru) + + async def get_ami_sensor_reading(self, sensorname): + """Get an OEM sensor + + If software wants to model some OEM behavior as a 'sensor' without + doing SDR, this hook provides that ability. It should mimic + the behavior of 'get_sensor_reading' in command.py. + """ + async for sensor in self.get_sensor_data(): + if sensor.name == sensorname: + return sensor + + async def get_ami_sensor_descriptions(self): + """Get list of OEM sensor names and types + + Iterate over dicts describing a label and type for OEM 'sensors'. This + should mimic the behavior of the get_sensor_descriptions function + in command.py. + """ + if await self.has_ami(): + energy_sensor = energy.Energy(self.ipmicmd) + async for sensor in energy_sensor.get_energy_sensor(): + yield {'name': sensor.name, + 'type': sensor.type} + + async def get_ami_sensor_data(self): + """Get OEM sensor data + + Iterate through all OEM 'sensors' and return data as if they were + normal sensors. This should mimic the behavior of the get_sensor_data + function in command.py. + """ + if await self.has_ami(): + energy_sensor = energy.Energy(self.ipmicmd) + async for sensor in energy_sensor.get_energy_sensor(): + yield sensor diff --git a/confluent_server/aiohmi/ipmi/oem/lenovo/imm.py b/confluent_server/aiohmi/ipmi/oem/lenovo/imm.py new file mode 100644 index 00000000..02b58260 --- /dev/null +++ b/confluent_server/aiohmi/ipmi/oem/lenovo/imm.py @@ -0,0 +1,2648 @@ +# coding=utf8 +# Copyright 2016-2023 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. + +import asyncio +import base64 +import binascii +from datetime import datetime +import errno +import fnmatch +import json +import math +import os.path +import random +import re +import socket +import struct +import weakref + +import six +import zipfile + +import aiohmi.constants as pygconst +import aiohmi.exceptions as pygexc +import aiohmi.ipmi.oem.lenovo.config as config +import aiohmi.ipmi.oem.lenovo.energy as energy +import aiohmi.ipmi.private.session as ipmisession +import aiohmi.ipmi.private.util as util +from aiohmi.ipmi import sdr +import aiohmi.media as media +import aiohmi.storage as storage +from aiohmi.util.parse import parse_time +import aiohmi.util.webclient as webclient + +try: + from urllib import urlencode +except ImportError: + from urllib.parse import urlencode + + def set_identify(self, on, duration, blink): + if blink: + self.grab_redfish_response_with_status( + '/redfish/v1/Systems/1', + {'IndicatorLED': 'Blinking'}, + method='PATCH') + raise pygexc.BypassGenericBehavior() + + + +numregex = re.compile('([0-9]+)') +funtypes = { + 0: 'RAID Controller', + 1: 'Ethernet', + 2: 'Fibre Channel', + 3: 'Infiniband', + 4: 'GPU', + 10: 'NVMe Controller', + 12: 'Fabric Controller', +} + + +def naturalize_string(key): + """Analyzes string in a human way to enable natural sort + + :param key: string for the split + :returns: A structure that can be consumed by 'sorted' + """ + return [int(text) if text.isdigit() else text.lower() + for text in re.split(numregex, key)] + + +def natural_sort(iterable): + """Return a sort using natural sort if possible + + :param iterable: + :return: + """ + try: + return sorted(iterable, key=naturalize_string) + except TypeError: + # The natural sort attempt failed, fallback to ascii sort + return sorted(iterable) + + +def fixup_uuid(uuidprop): + baduuid = ''.join(uuidprop.split()) + uuidprefix = (baduuid[:8], baduuid[8:12], baduuid[12:16]) + a = struct.pack('