Add custom OEM inventory parsing for Lenovo ThinkServers

Parse OEM data for CPU, DIMM, PCI, drive and PSU inventory.

Change-Id: Ic75d03fb1b05c13702321d174c8dbb9a96a08ae4
This commit is contained in:
Allan Vidal 2015-08-14 12:04:55 -03:00
parent 74f7c4d936
commit ea3960c226
11 changed files with 737 additions and 2 deletions

View File

@ -64,6 +64,9 @@ def docommand(result, ipmisession):
print repr(reading)
elif cmmand == 'health':
print repr(ipmisession.get_health())
elif cmmand == 'inventory':
for item in ipmisession.get_inventory():
print repr(item)
elif cmmand == 'raw':
print ipmisession.raw_command(netfn=int(args[0]),
command=int(args[1]),

View File

@ -1417,7 +1417,7 @@ class Command(object):
channel = self.get_network_channel()
names = {}
max_ids = self.get_channel_max_user_count(channel)
for uid in range(1, max_ids):
for uid in range(1, max_ids+1):
name = self.get_user_name(uid=uid)
if name is not None:
names[uid] = self.get_user(uid=uid, channel=channel)

View File

48
pyghmi/ipmi/oem/lenovo/cpu.py Executable file
View File

@ -0,0 +1,48 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from pyghmi.ipmi.oem.lenovo.inventory import EntryField, \
parse_inventory_category_entry
cpu_fields = (
EntryField("index", "B"),
EntryField("Cores", "B"),
EntryField("Threads", "B"),
EntryField("Manufacturer", "13s"),
EntryField("Family", "30s"),
EntryField("Model", "30s"),
EntryField("Stepping", "3s"),
EntryField("Maximum Frequency", "<I",
valuefunc=lambda v: str(v) + " MHz"),
EntryField("Reserved", "h", include=False))
def parse_cpu_info(raw):
return parse_inventory_category_entry(raw, cpu_fields)
def get_categories():
return {
"cpu": {
"idstr": "CPU {0}",
"parser": parse_cpu_info,
"command": {
"netfn": 0x06,
"command": 0x59,
"data": (0x00, 0xc1, 0x01, 0x00)
}
}
}

52
pyghmi/ipmi/oem/lenovo/dimm.py Executable file
View File

@ -0,0 +1,52 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from pyghmi.ipmi.oem.lenovo.inventory import EntryField, \
parse_inventory_category_entry
dimm_fields = (
EntryField("index", "B"),
EntryField("manufacture_location", "B"),
EntryField("channel_number", "B"),
EntryField("module_type", "10s"),
EntryField("ddr_voltage", "10s"),
EntryField("speed", "<h",
valuefunc=lambda v: str(v) + " MHz"),
EntryField("capacity_mb", "<h",
valuefunc=lambda v: v*1024),
EntryField("manufacturer", "30s"),
EntryField("serial", "I"),
EntryField("model", "21s"),
EntryField("reserved", "h", include=False)
)
def parse_dimm_info(raw):
return parse_inventory_category_entry(raw, dimm_fields)
def get_categories():
return {
"dimm": {
"idstr": "DIMM {0}",
"parser": parse_dimm_info,
"command": {
"netfn": 0x06,
"command": 0x59,
"data": (0x00, 0xc1, 0x02, 0x00)
}
}
}

73
pyghmi/ipmi/oem/lenovo/drive.py Executable file
View File

@ -0,0 +1,73 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from pyghmi.ipmi.oem.lenovo.inventory import EntryField, \
parse_inventory_category_entry
drive_fields = (
EntryField("index", "B"),
EntryField("VendorID", "64s"),
EntryField("Size", "I",
valuefunc=lambda v: str(v) + " MB"),
EntryField("MediaType", "B", mapper={
0x00: "HDD",
0x01: "SSD"
}),
EntryField("InterfaceType", "B", mapper={
0x00: "Unknown",
0x01: "ParallelSCSI",
0x02: "SAS",
0x03: "SATA",
0x04: "FC"
}),
EntryField("FormFactor", "B", mapper={
0x00: "Unknown",
0x01: "2.5in",
0x02: "3.5in"
}),
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"
}),
EntryField("SlotNumber", "B"),
EntryField("DeviceState", "B", mapper={
0x00: "active",
0x01: "stopped",
0xff: "transitioning"
}),
# There seems to be an undocumented byte at the end
EntryField("Reserved", "B", include=False))
def parse_drive_info(raw):
return 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)
}
}
}

248
pyghmi/ipmi/oem/lenovo/handler.py Executable file
View File

@ -0,0 +1,248 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import traceback
import pyghmi.constants as pygconst
import pyghmi.exceptions as pygexc
import pyghmi.ipmi.oem.generic as generic
import pyghmi.ipmi.private.constants as ipmiconst
import pyghmi.ipmi.private.util as util
from pyghmi.ipmi.oem.lenovo import cpu
from pyghmi.ipmi.oem.lenovo import dimm
from pyghmi.ipmi.oem.lenovo import drive
from pyghmi.ipmi.oem.lenovo import inventory
from pyghmi.ipmi.oem.lenovo import pci
from pyghmi.ipmi.oem.lenovo import psu
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)
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),
}
class OEMHandler(generic.OEMHandler):
# noinspection PyUnusedLocal
def __init__(self, oemid, ipmicmd):
# will need to retain data to differentiate
# variations. For example System X versus Thinkserver
self.oemid = oemid
self.ipmicmd = ipmicmd
self.oem_inventory_info = None
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)
@property
def has_tsm(self):
"""True if this particular server have a TSM based service processor
"""
return (self.oemid['manufacturer_id'] == 19046 and
self.oemid['device_id'] == 32)
def get_oem_inventory_descriptions(self):
if self.has_tsm:
# Thinkserver with TSM
if not self.oem_inventory_info:
self._collect_tsm_inventory()
return iter(self.oem_inventory_info)
return ()
def get_oem_inventory(self):
if self.has_tsm:
self._collect_tsm_inventory()
for compname in self.oem_inventory_info:
yield (compname, self.oem_inventory_info[compname])
def get_inventory_of_component(self, component):
if self.has_tsm:
self._collect_tsm_inventory()
return self.oem_inventory_info.get(component, None)
def _collect_tsm_inventory(self):
self.oem_inventory_info = {}
for catid, catspec in inventory.categories.items():
try:
rsp = self.ipmicmd.xraw_command(**catspec["command"])
except pygexc.IpmiException:
continue
else:
try:
items = inventory.parse_inventory_category(catid, rsp)
except Exception:
# If we can't parse an inventory category, ignore it
print traceback.print_exc()
continue
for item in items:
try:
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
def process_fru(self, fru):
if fru is None:
return fru
if self.has_tsm:
fru['oem_parser'] = 'lenovo'
# Thinkserver lays out specific interpretation of the
# board extra fields
_, _, wwn1, wwn2, mac1, mac2 = fru['board_extra']
if wwn1 not in ('0000000000000000', ''):
fru['WWN 1'] = wwn1
if wwn2 not in ('0000000000000000', ''):
fru['WWN 2'] = wwn2
if mac1 not in ('00:00:00:00:00:00', ''):
fru['MAC Address 1'] = mac1
if mac2 not in ('00:00:00:00:00:00', ''):
fru['MAC Address 2'] = mac2
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)))
fru['UUID'] = util.decode_wireformat_uuid(byteguid)
except (AttributeError, KeyError):
pass
return fru
else:
fru['oem_parser'] = None
return fru

View File

@ -0,0 +1,124 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import struct
categories = {}
def register_inventory_category(module):
c = module.get_categories()
for id in c:
categories[id] = c[id]
class EntryField(object):
"""Store inventory field parsing options.
Represents an inventory field and its options for the custom requests to a
ThinkServer's BMC.
:param name: the name of the field
:param fmt: the format of the field (see struct module for details)
:param include: whether to include the field in the parse output
:param mapper: a dictionary mapping values to new values for the parse
output
:param valuefunc: a function to be called to change the value in the last
step of the build process.
"""
def __init__(self, name, fmt, include=True, mapper=None, valuefunc=None,
multivaluefunc=False):
self.name = name
self.fmt = fmt
self.include = include
self.mapper = mapper
self.valuefunc = valuefunc
self.multivaluefunc = multivaluefunc
# General parameter parsing functions
def parse_inventory_category(name, info):
"""Parses every entry in an inventory category (CPU, memory, PCI, drives,
etc).
Expects the first byte to be a count of the number of entries, followed
by a list of elements to be parsed by a dedicated parser (below).
:param name: the name of the parameter (e.g.: "cpu")
:param info: a list of integers with raw data read from an IPMI requests
:returns: dict -- a list of entries in the category.
"""
raw = info["data"][1:]
cur = 0
count = struct.unpack("B", raw[cur])[0]
cur += 1
entries = []
while cur < len(raw):
read, cpu = categories[name]["parser"](raw[cur:])
cur = cur + read
entries.append(cpu)
# TODO(avidal): raise specific exception to point that there's data left in
# the buffer
if cur != len(raw):
raise Exception
# TODO(avidal): raise specific exception to point that the number of
# entries is different than the expected
if count != len(entries):
raise Exception
return entries
def parse_inventory_category_entry(raw, fields):
"""Parses one entry in an inventory category.
:param raw: the raw data to the entry. May contain more than one entry,
only one entry will be read in that case.
:param fields: an iterable of EntryField objects to be used for parsing the
entry.
:returns: dict -- a tuple with the number of bytes read and a dictionary
representing the entry.
"""
r = raw
obj = {}
bytes_read = 0
for field in fields:
value = struct.unpack_from(field.fmt, r)[0]
read = struct.calcsize(field.fmt)
bytes_read += read
r = r[read:]
if not field.include:
continue
if (field.fmt[-1] == "s"):
value = value.rstrip("\x00")
if (field.mapper and value in field.mapper):
value = field.mapper[value]
if (field.valuefunc):
value = field.valuefunc(value)
if not field.multivaluefunc:
obj[field.name] = value
else:
for key in value:
obj[key] = value[key]
return bytes_read, obj

64
pyghmi/ipmi/oem/lenovo/pci.py Executable file
View File

@ -0,0 +1,64 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from pyghmi.ipmi.oem.lenovo.inventory import EntryField, \
parse_inventory_category_entry
pci_fields = (
EntryField("index", "B"),
EntryField("PCIType", "B", mapper={
0x0: "On board slot",
0x1: "Riser Type 1",
0x2: "Riser Type 2",
0x3: "Riser Type 3",
0x4: "Riser Type 4",
0x5: "Riser Type 5",
0x6: "Riser Type 6a",
0x7: "Riser Type 6b",
0x8: "ROC",
0x9: "Mezz"
}),
EntryField("BusNumber", "B"),
EntryField("DeviceFunction", "B"),
EntryField("VendorID", "<H"),
EntryField("DeviceID", "<H"),
EntryField("SubSystemVendorID", "<H"),
EntryField("SubSystemID", "<H"),
EntryField("InterfaceType", "B"),
EntryField("SubClassCode", "B"),
EntryField("BaseClassCode", "B"),
EntryField("LinkSpeed", "B"),
EntryField("LinkWidth", "B"),
EntryField("Reserved", "h")
)
def parse_pci_info(raw):
return parse_inventory_category_entry(raw, pci_fields)
def get_categories():
return {
"pci": {
"idstr": "PCI {0}",
"parser": parse_pci_info,
"command": {
"netfn": 0x06,
"command": 0x59,
"data": (0x00, 0xc1, 0x03, 0x00)
}
}
}

123
pyghmi/ipmi/oem/lenovo/psu.py Executable file
View File

@ -0,0 +1,123 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from pyghmi.ipmi.oem.lenovo.inventory import EntryField, \
parse_inventory_category_entry
psu_type = {
0b0001: "Other",
0b0010: "Unknown",
0b0011: "Linear",
0b0100: "Switching",
0b0101: "Battery",
0b0110: "UPS",
0b0111: "Converter",
0b1000: "Regulator",
}
psu_status = {
0b001: "Other",
0b010: "Unknown",
0b011: "OK",
0b100: "Non-critical",
0b101: "Critical; power supply has failed and has been taken off-line"
}
psu_voltage_range_switch = {
0b0001: "Other",
0b0010: "Unknown",
0b0011: "Manual",
0b0100: "Auto-switch",
0b0101: "Wide range",
0b0110: "Not applicable"
}
def psu_status_word_slice(w, s, e):
return int(w[-e-1:-s], 2)
def psu_status_word_bit(w, b):
return int(w[-b-1])
def psu_status_word_parser(word):
fields = {}
word = "{0:016b}".format(word)
fields["DMTF Power Supply Type"] = \
psu_type.get(psu_status_word_slice(word, 10, 13), "Invalid")
# fields["Status"] = \
# psu_status.get(psu_status_word_slice(word, 7, 9), "Invalid")
fields["DMTF Input Voltage Range"] = \
psu_voltage_range_switch.get(
psu_status_word_slice(word, 3, 6),
"Invalid"
)
# Power supply is unplugged from the wall
fields["Unplugged"] = \
bool(psu_status_word_bit(word, 2))
# fields["Power supply is present"] = \
# bool(psu_status_word_bit(word, 1))
# Power supply is hot-replaceable
fields["Hot Replaceable"] = \
bool(psu_status_word_bit(word, 0))
return fields
psu_fields = (
EntryField("index", "B"),
EntryField("Presence State", "B", include=False),
EntryField("Capacity W", "<H"),
EntryField("Board manufacturer", "18s"),
EntryField("Board model", "18s"),
EntryField("Board manufacture date", "10s"),
EntryField("Board serial number", "34s"),
EntryField("Board manufacturer revision", "5s"),
EntryField("Board product name", "10s"),
EntryField("PSU Asset Tag", "10s"),
EntryField(
"PSU Redundancy Status",
"B",
valuefunc=lambda v: "Not redundant" if v == 0x00 else "Redundant"
),
EntryField(
"PSU Status Word",
"<H",
valuefunc=psu_status_word_parser, multivaluefunc=True
)
)
def parse_psu_info(raw):
return parse_inventory_category_entry(raw, psu_fields)
def get_categories():
return {
"psu": {
"idstr": "Power Supply {0}",
"parser": parse_psu_info,
"command": {
"netfn": 0x06,
"command": 0x59,
"data": (0x00, 0xc6, 0x00, 0x00)
}
}
}

2
pyghmi/ipmi/oem/lookup.py Normal file → Executable file
View File

@ -13,7 +13,7 @@
# limitations under the License.
import pyghmi.ipmi.oem.generic as generic
import pyghmi.ipmi.oem.lenovo as lenovo
import pyghmi.ipmi.oem.lenovo.handler as lenovo
# The mapping comes from
# http://www.iana.org/assignments/enterprise-numbers/enterprise-numbers