blob: b1efbd3a04cde1e71377c7cbe1b0d2b8ba709ec4 [file] [log] [blame] [edit]
#!/usr/bin/python
#
# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import os
import pprint
import re
import sys
import flashrom_util
import gft_common
import gft_fwhash
import vblock
from gft_common import DebugMsg, VerboseMsg, WarningMsg, ErrorMsg, ErrorDie
class HardwareComponents(object):
""" Hardware Components Scanner """
# Function names in this class are used for reflection, so please don't change
# the function names even if they are not comliant to coding style guide.
version = 2
# We divide all component IDs (cids) into 5 categories:
# - enumerable: able to get the results by running specific commands;
# - PCI: PCI devices;
# - USB: USB devices;
# - probable: returns existed or not by given some pre-defined choices;
# - pure data: data for some special purpose, can't be tested;
_enumerable_cids = [
'data_display_geometry',
'hash_ec_firmware',
'hash_ro_firmware',
'part_id_audio_codec',
'part_id_cpu',
'part_id_display_panel',
'part_id_dram',
'part_id_embedded_controller',
'part_id_ethernet',
'part_id_flash_chip',
'part_id_ec_flash_chip',
'part_id_hwqual',
'part_id_storage',
'part_id_tpm',
'part_id_wireless',
'vendor_id_touchpad',
'version_rw_firmware',
]
_pci_cids = [
'part_id_chipset',
'part_id_usb_hosts',
'part_id_vga',
]
_usb_cids = [
'part_id_bluetooth',
'part_id_webcam',
'part_id_3g',
'part_id_gps',
]
_probable_cids = [
'key_recovery',
'key_root',
'part_id_cardreader',
'part_id_chrontel',
]
_pure_data_cids = [
'data_bitmap_fv',
'data_recovery_url',
]
# _not_test_cids and _to_be_tested_cids will be re-created for each match.
_not_test_cids = []
_to_be_tested_cids = []
_not_present = 'Not Present'
def __init__(self, verbose=False):
self._initialized = False
self._verbose = verbose
self._pp = pprint.PrettyPrinter()
# cache for firmware images
self._flashrom = flashrom_util.flashrom_util(
verbose_msg=VerboseMsg,
exception_type=gft_common.GFTError,
system_output=gft_common.SystemOutput)
self._bios_image_file = None
self._ec_image_file = None
self._bios_flash_id = None
self._ec_flash_id = None
# variables for matching
self._enumerable_system = None
self._pci_system = None
self._usb_system = None
self._file_base = None
self._system = None
self._failures = None
def __del__(self):
try:
if self._bios_image_file:
os.remove(self._bios_image_file)
if self._ec_image_file:
os.remove(self._ec_image_file)
except:
pass
def get_all_enumerable_components(self):
results = {}
for cid in self._enumerable_cids:
if self._verbose:
sys.stdout.flush()
sys.stderr.write('<Fetching property %s>\n' % cid)
components = self.force_get_property('get_' + cid)
if not isinstance(components, list):
components = [components]
results[cid] = components
return results
def get_all_pci_components(self):
cmd = 'lspci -n | cut -f3 -d" "'
return gft_common.SystemOutput(cmd, progress_messsage='Probing PCI: ',
show_progress=self._verbose).splitlines()
def get_all_usb_components(self):
cmd = 'lsusb | cut -f6 -d" "'
return gft_common.SystemOutput(cmd, progress_messsage='Probing USB: ',
show_progress=self._verbose).splitlines()
def check_enumerable_component(self, cid, exact_values, approved_values):
if '*' in approved_values:
return
for value in exact_values:
if value not in approved_values:
if cid in self._failures:
self._failures[cid].append(value)
else:
self._failures[cid] = [value]
def check_pci_usb_component(self, cid, system_values, approved_values):
if '*' in approved_values:
self._system[cid] = ['*']
return
for value in approved_values:
if value in system_values:
self._system[cid] = [value]
return
self._failures[cid] = ['No match']
def check_probable_component(self, cid, approved_values):
if '*' in approved_values:
self._system[cid] = ['*']
return
for value in approved_values:
present = getattr(self, 'probe_' + cid)(value)
if present:
self._system[cid] = [value]
return
self._failures[cid] = ['No match']
def get_data_display_geometry(self):
# Get edid from driver.
# TODO(nsanders): this is driver specific.
# TODO(waihong): read-edid is also x86 only.
# format: Mode "1280x800" -> 1280x800
cmd = ('cat "$(find /sys/devices/ -name edid | grep LVDS)" | '
'parse-edid 2>/dev/null | grep "Mode " | cut -d\\" -f2')
output = gft_common.SystemOutput(cmd).splitlines()
return (output if output else [''])
def get_hash_ec_firmware(self):
"""
Returns a hash of Embedded Controller firmware parts,
to confirm we have proper updated version of EC firmware.
"""
if not self.load_ec_firmware():
ErrorDie('get_hash_ec_firmware: cannot read firmware')
return gft_fwhash.GetECHash(file_source=self._ec_image_file)
def get_hash_ro_firmware(self):
"""
Returns a hash of Read Only (BIOS) firmware parts,
to confirm we have proper keys / boot code / recovery image installed.
"""
image_file = self.load_main_firmware()
if not image_file:
ErrorDie('get_hash_ro_firmware: cannot read firmware')
return gft_fwhash.GetBIOSReadOnlyHash(file_source=image_file)
def get_part_id_audio_codec(self):
cmd = 'grep -R Codec: /proc/asound/* | head -n 1 | sed s/.\*Codec://'
part_id = gft_common.SystemOutput(
cmd, progress_messsage='Searching Audio Codecs: ',
show_progress=self._verbose).strip()
return part_id
def get_part_id_cpu(self):
cmd = 'grep -m 1 "model name" /proc/cpuinfo | sed s/.\*://'
part_id = gft_common.SystemOutput(cmd).strip()
return part_id
def get_part_id_display_panel(self):
# format: ModelName "SEC:4231" -> SEC:4231
cmd = ('cat "$(find /sys/devices/ -name edid | grep LVDS)" | '
'parse-edid 2>/dev/null | grep ModelName | cut -d\\" -f2')
output = gft_common.SystemOutput(cmd).strip()
return (output if output else self._not_present)
def get_part_id_embedded_controller(self):
# example output:
# Found Nuvoton WPCE775x (id=0x05, rev=0x02) at 0x2e
parts = []
res = gft_common.SystemOutput(
'superiotool',
progress_messsage='Probing Embedded Controller: ',
show_progress=self._verbose,
ignore_status=True).splitlines()
for line in res:
match = re.search(r'Found (.*) at', line)
if match:
parts.append(match.group(1))
part_id = ', '.join(parts)
return part_id
def get_part_id_ethernet(self):
"""
Returns a colon delimited string where the first section
is the vendor id and the second section is the device id.
"""
# Ethernet is optional so mark it as not present. A human
# operator needs to decide if this is acceptable or not.
vendor_file = '/sys/class/net/eth0/device/vendor'
part_file = '/sys/class/net/eth0/device/device'
if os.path.exists(part_file) and os.path.exists(vendor_file):
vendor_id = gft_common.ReadOneLine(vendor_file).replace('0x', '')
part_id = gft_common.ReadOneLine(part_file).replace('0x', '')
return '%s:%s' % (vendor_id, part_id)
else:
return self._not_present
def get_part_id_dram(self):
grep_cmd = 'lsmod | grep -q i2c_dev'
i2c_loaded = (os.system(grep_cmd) == 0)
if not i2c_loaded:
os.system('modprobe i2c_dev >/dev/null 2>&1')
cmd = ('mosys -l memory spd print geometry | '
'grep size_mb | cut -f2 -d"|"')
part_id = gft_common.SystemOutput(cmd).strip()
if not i2c_loaded:
os.system('modprobe -r i2c_dev >/dev/null 2>&1')
if part_id != '':
return part_id
else:
return self._not_present
def get_part_id_flash_chip(self):
if not self.load_main_firmware():
return ''
return self._bios_flash_id
def get_part_id_ec_flash_chip(self):
if not self.load_ec_firmware():
return ''
return self._ec_flash_id
def get_part_id_hwqual(self):
part_id = gft_common.SystemOutput('crossystem hwid').strip()
return (part_id if part_id else self._not_present)
def get_part_id_storage(self):
cmd = ('cd $(find /sys/devices -name sda)/../..; '
'cat vendor model | tr "\n" " " | sed "s/ \\+/ /g"')
part_id = gft_common.SystemOutput(cmd).strip()
return part_id
def get_part_id_tpm(self):
""" Returns Manufacturer_info : Chip_Version """
cmd = 'tpm_version'
tpm_output = gft_common.SystemOutput(cmd)
tpm_lines = tpm_output.splitlines()
tpm_dict = {}
for tpm_line in tpm_lines:
[key, colon, value] = tpm_line.partition(':')
tpm_dict[key.strip()] = value.strip()
part_id = ''
(key1, key2) = ('Manufacturer Info', 'Chip Version')
if key1 in tpm_dict and key2 in tpm_dict:
part_id = tpm_dict[key1] + ':' + tpm_dict[key2]
return part_id
def get_part_id_wireless(self):
"""
Returns a colon delimited string where the first section
is the vendor id and the second section is the device id.
"""
part_id = gft_common.ReadOneLine('/sys/class/net/wlan0/device/device')
vendor_id = gft_common.ReadOneLine('/sys/class/net/wlan0/device/vendor')
return '%s:%s' % (vendor_id.replace('0x', ''), part_id.replace('0x', ''))
def get_closed_vendor_id_touchpad(self, vendor_name):
""" Using closed-source method to derive vendor information by name. """
part_id = ''
if vendor_name.lower() == 'synaptics':
detect_program = '/opt/Synaptics/bin/syndetect'
model_string_str = 'Model String'
firmware_id_str = 'Firmware ID'
if os.path.exists(detect_program):
data = gft_common.SystemOutput(
detect_program,
progress_messsage='Synaptics Touchpad: ',
show_progress=self._verbose,
ignore_status=True)
properties = dict(map(str.strip, line.split('=', 1))
for line in data.splitlines() if '=' in line)
model = properties.get(model_string_str, 'UnknownModel')
firmware_id = properties.get(firmware_id_str, 'UnknownFWID')
# The pattern " on xxx Port" may vary by the detection approach,
# so we need to strip it.
model = re.sub(' on [^ ]* [Pp]ort$', '', model)
# Format: Model #FirmwareId
part_id = '%s #%s' % (model, firmware_id)
return part_id
def get_vendor_id_touchpad(self):
# First, try to use closed-source method to probe touch pad
part_id = self.get_closed_vendor_id_touchpad('Synaptics')
if part_id != '':
return part_id
else:
# If the closed-source method above fails to find vendor infomation,
# try an open-source method.
cmd_grep = 'grep -i Touchpad /proc/bus/input/devices | sed s/.\*=//'
part_id = gft_common.SystemOutput(
cmd_grep,
progress_messsage='Finding Touchpad: ',
show_progress=self._verbose).strip('"')
return part_id
def get_vendor_id_webcam(self):
cmd = 'cat /sys/class/video4linux/video0/name'
part_id = gft_common.SystemOutput(cmd).strip()
return part_id
def get_version_rw_firmware(self):
"""
Returns the version of Read-Write (writable) firmware from VBLOCK sections.
If A/B has different version, that means this system needs a reboot +
firmwar update so return value is a "error report" in the form "A=x, B=y".
"""
versions = [None, None]
section_names = ['VBLOCK_A', 'VBLOCK_B']
image_file = self.load_main_firmware()
if not image_file:
ErrorDie('Cannot read system main firmware.')
flashrom = self._flashrom
base_img = open(image_file).read()
flashrom_size = len(base_img)
# we can trust base image for layout, since it's only RW.
layout = flashrom.detect_chromeos_bios_layout(flashrom_size, base_img)
if not layout:
ErrorDie('Cannot detect ChromeOS flashrom layout')
for (index, name) in enumerate(section_names):
data = flashrom.get_section(base_img, layout, name)
block = vblock.unpack_verification_block(data)
ver = block['VbFirmwarePreambleHeader']['firmware_version']
versions[index] = ver
# we embed error reports in return value.
assert len(versions) == 2
if versions[0] != versions[1]:
return 'A=%d, B=%d' % (versions[0], versions[1])
return '%d' % versions[0]
def _read_gbb_component(self, name):
image_file = self.load_main_firmware()
if not image_file:
ErrorDie('cannot load main firmware')
filename = gft_common.GetTemporaryFileName('gbb%s' % name)
if os.system('gbb_utility -g --%s=%s %s >/dev/null 2>&1' %
(name, filename, image_file)) != 0:
ErrorDie('cannot get %s from GBB' % name)
value = gft_common.ReadBinaryFile(filename)
os.remove(filename)
return value
def probe_key_recovery(self, part_id):
current_key = self._read_gbb_component('recoverykey')
file_path = os.path.join(self._file_base, part_id)
target_key = gft_common.ReadBinaryFile(file_path)
return current_key.startswith(target_key)
def probe_key_root(self, part_id):
current_key = self._read_gbb_component('rootkey')
file_path = os.path.join(self._file_base, part_id)
target_key = gft_common.ReadBinaryFile(file_path)
return current_key.startswith(target_key)
def probe_part_id_cardreader(self, part_id):
# A cardreader is always power off until a card inserted. So checking
# it using log messages instead of lsusb can limit operator-attended.
# But note that it does not guarantee the cardreader presented during
# the time of the test.
[vendor_id, product_id] = part_id.split(':')
found_pattern = ('New USB device found, idVendor=%s, idProduct=%s' %
(vendor_id, product_id))
cmd = 'grep -qs "%s" /var/log/messages*' % found_pattern
return os.system(cmd) == 0
def probe_part_id_chrontel(self, part_id):
if part_id == self._not_present:
return True
if part_id == 'ch7036':
grep_cmd = 'grep i2c_dev /proc/modules'
i2c_loaded = os.system(grep_cmd) == 0
if not i2c_loaded:
os.system('modprobe i2c_dev')
probe_cmd = 'ch7036_monitor -p'
present = os.system(probe_cmd) == 0
if not i2c_loaded:
os.system('modprobe -r i2c_dev')
return present
return False
def force_get_property(self, property_name):
""" Returns property value or empty string on error. """
try:
return getattr(self, property_name)()
except gft_common.GFTError, e:
ErrorMsg("Error in probing property %s: %s" % (property_name, e.value))
return ''
except:
ErrorMsg('Exception in getting property %s' % property_name)
return ''
def pformat(self, obj):
return '\n' + self._pp.pformat(obj) + '\n'
def update_ignored_cids(self, ignored_cids):
for cid in ignored_cids:
if cid in self._to_be_tested_cids:
self._to_be_tested_cids.remove(cid)
DebugMsg('Ignoring cid: %s' % cid)
else:
ErrorDie('The ignored cid %s is not defined' % cid)
self._not_test_cids.append(cid)
def read_approved_from_file(self, filename):
approved = gft_common.LoadComponentsDatabaseFile(filename)
for cid in self._to_be_tested_cids + self._not_test_cids:
if cid not in approved:
# If we don't have any listing for this type
# of part in HWID, it's not required.
WarningMsg('gft_hwcomp: Bypassing unlisted cid %s' % cid)
approved[cid] = '*'
return approved
def _load_firmware(self, target_name, target_option):
filename = gft_common.GetTemporaryFileName('fw_cache')
# example output:
# Found chip "Winbond W25x16" (2048 KB, FWH) at physical address 0xfe
# TODO(hungte) maybe we don't need the -V in future -- if that can make
# the command faster.
command = 'flashrom -V %s -r %s' % (target_option, filename)
parts = []
lines = gft_common.SystemOutput(
command,
progress_messsage='Reading %s: ' % target_name,
show_progress=self._verbose).splitlines()
for line in lines:
match = re.search(r'Found chip "(.*)" .* at physical address ', line)
if match:
parts.append(match.group(1))
part_id = ', '.join(parts)
return (part_id, filename)
def load_main_firmware(self):
""" Loads and cache main (BIOS) firmware image.
Returns:
A file name of cached image.
"""
if self._bios_flash_id:
return self._bios_image_file
(self._bios_flash_id, self._bios_image_file) = self._load_firmware(
'BIOS', '-p internal:bus=spi')
return self._bios_image_file
def load_ec_firmware(self):
""" Loads and cache EC firmware image.
Returns:
A file name of cached image.
"""
if self._ec_flash_id:
return self._ec_image_file
(self._ec_flash_id, self._ec_image_file) = self._load_firmware(
'EC', '-p internal:bus=lpc')
# restore flashrom target bus
os.system('flashrom -p internal:bus=spi >/dev/null 2>&1')
return self._ec_image_file
def initialize(self, force=False):
if self._initialized and not force:
return
# probe current system components
DebugMsg('Starting to probe system components...')
self._enumerable_system = self.get_all_enumerable_components()
self._pci_system = self.get_all_pci_components()
self._usb_system = self.get_all_usb_components()
self._initialized = True
def match_current_system(self, filename, ignored_cids=[]):
""" Matches a given component list to current system.
Returns: (current, failures)
"""
# assert self._initialized, 'Not initialized.'
self._to_be_tested_cids = (self._enumerable_cids +
self._pci_cids +
self._usb_cids +
self._probable_cids)
self._not_test_cids = self._pure_data_cids[:]
self.update_ignored_cids(ignored_cids)
self._failures = {}
self._system = {}
self._system.update(self._enumerable_system)
self._file_base = gft_common.GetComponentsDatabaseBase(filename)
approved = self.read_approved_from_file(filename)
for cid in self._enumerable_cids:
if cid not in self._to_be_tested_cids:
VerboseMsg('gft_hwcomp: match: ignored: %s' % cid)
else:
self.check_enumerable_component(
cid, self._enumerable_system[cid], approved[cid])
for cid in self._pci_cids:
if cid not in self._to_be_tested_cids:
VerboseMsg('gft_hwcomp: match: ignored: %s' % cid)
else:
self.check_pci_usb_component(cid, self._pci_system, approved[cid])
for cid in self._usb_cids:
if cid not in self._to_be_tested_cids:
VerboseMsg('gft_hwcomp: match: ignored: %s' % cid)
else:
self.check_pci_usb_component(cid, self._usb_system, approved[cid])
for cid in self._probable_cids:
if cid not in self._to_be_tested_cids:
VerboseMsg('gft_hwcomp: match: ignored: %s' % cid)
else:
self.check_probable_component(cid, approved[cid])
return (self._system, self._failures)
#############################################################################
# Console main entry
@gft_common.GFTConsole
def _main(self_path, args):
if not args:
print 'Usage: %s components_db_files...\n' % self_path
sys.exit(1)
components = HardwareComponents(verbose=True)
print 'Starting to probe system...'
components.initialize()
print 'Starting to match system...'
for arg in args:
(matched, failures) = components.match_current_system(arg)
print 'Probed (%s):' % arg
print components.pformat(matched)
print 'Failures (%s):' % arg
print components.pformat(failures)
if __name__ == '__main__':
_main(sys.argv[0], sys.argv[1:])