blob: 65c1a175f99fd5bf96c7e4d3b99d02a83ca0bd96 [file] [log] [blame]
# Copyright 2017 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 codecs
import glob
import logging
import os
import re
import string # pylint: disable=deprecated-module
import struct
from cros.factory.probe.functions import file as file_module
from cros.factory.probe.functions import sysfs
from cros.factory.probe.lib import cached_probe_function
from cros.factory.utils import file_utils
from cros.factory.utils import process_utils
STORAGE_SYSFS_PATH = '/sys/class/block/*'
SMARTCTL_PATH = '/usr/sbin/smartctl'
def GetFixedDevices():
"""Returns paths to all fixed storage devices on the system."""
ret = []
for node in sorted(glob.glob(STORAGE_SYSFS_PATH)):
path = os.path.join(node, 'removable')
if not os.path.exists(path) or file_utils.ReadFile(path).strip() != '0':
continue
if re.match(r'^loop|^dm-', os.path.basename(node)):
# Loopback or dm-verity device; skip
continue
ret.append(node)
return ret
def CookVersion(raw_version):
"""Returns human readable version string."""
version = ''.join('%02x' % c for c in raw_version)
# The output for firmware version is encoded by hexdump of a ASCII
# string or hexdump of hexadecimal values, always in 8 characters.
# For example, version 'ABCDEFGH' is:
# [raw_version[7]]: 0x48
# [raw_version[6]]: 0x47
# [raw_version[5]]: 0x46
# [raw_version[4]]: 0x45
# [raw_version[3]]: 0x44
# [raw_version[2]]: 0x43
# [raw_version[1]]: 0x42
# [raw_version[0]]: 0x41
#
# Some vendors might use hexadecimal values for it.
# For example, version 3 is:
# [raw_version[7]]: 0x00
# [raw_version[6]]: 0x00
# [raw_version[5]]: 0x00
# [raw_version[4]]: 0x00
# [raw_version[3]]: 0x00
# [raw_version[2]]: 0x00
# [raw_version[1]]: 0x00
# [raw_version[0]]: 0x03
#
# To handle both cases, this function returns a 64-bit hexadecimal value
# and will try to decode it as a ASCII string or as a 64-bit little-endian
# integer. It returns '4142434445464748 (ABCDEFGH)' for the first example
# and returns '0300000000000000 (3)' for the second example.
# Try to decode it as a ASCII string.
# Note vendor may choose SPACE (0x20) or NUL (0x00) to pad version string,
# so we want to strip both in the human readable part.
ascii_string = ''.join(map(chr, raw_version)).strip(' \0')
if ascii_string and all(
(c in string.ascii_letters or c in string.digits) for c in ascii_string):
version += ' (%s)' % ascii_string
else:
# Try to decode it as a 64-bit little-endian integer.
version += ' (%s)' % struct.unpack_from('<q', codecs.decode(version,
'hex'))
return version
def GetStorageFirmwareVersion(node_path):
"""Extracts firmware version.
Args:
node_path: the node_path returned by GetFixedDevices(). For example,
'/sys/class/block/mmcblk0'.
Returns:
A string indicating the firmware version if firmware version is found.
Return None if firmware version doesn't present.
"""
dev_path = os.path.join(node_path, 'device')
# Use fwrev file (e.g., for eMMC)
emmc_fw_vr = file_module.ReadFile(os.path.join(dev_path, 'fwrev'))
if emmc_fw_vr:
# Cast a hex string to a 64-bit little-endian integer and then decode it to
# a list of 8 bytes.
# For exmaple, if emmc_fw_vr = '0x4142434445464748' then raw_version =
# [0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48]
raw_version = [((int(emmc_fw_vr, 0) >> ((7-i)*8)) & 255) for i in range(8)]
return CookVersion(raw_version)
# Use firmware_rev file (e.g., for NVMe)
# It is possible that the 8th byte is a space, and it will be stripped off if
# binary_mode is false.
nvme_fw_vr = file_module.ReadFile(os.path.join(dev_path, 'firmware_rev'),
binary_mode=True)
if nvme_fw_vr:
raw_version = [int(x, 0) for x in nvme_fw_vr.split(' ')[0:8]]
return CookVersion(raw_version)
# Use smartctl (e.g., for SATA)
sata_dev_path = os.path.join('/dev', os.path.basename(node_path))
smartctl = process_utils.SpawnOutput([SMARTCTL_PATH, '--all', sata_dev_path],
log=True)
matches = re.findall(r'(?m)^Firmware Version:\s+(.+)$', smartctl)
if matches:
cooked_version = matches[-1]
if re.search(r'(?m)^Device Model:\s+SanDisk', smartctl):
# Canonicalize SanDisk firmware versions by replacing 'CS' with '11'.
# According to b/35513546, 'CS's in firmware version of SanDisk are
# equivalent to '11's.
cooked_version = re.sub('^CS', '11', cooked_version)
return CookVersion(list(map(ord, cooked_version)))
return None
class GenericStorageFunction(cached_probe_function.CachedProbeFunction):
"""Probe the generic storage information."""
ATA_FIELDS = ['vendor', 'model']
EMMC_FIELDS = ['type', 'name', 'hwrev', 'oemid', 'manfid']
NVME_FIELDS = ['vendor', 'device', 'class']
# Another field 'cid' is a combination of all other fields so we should not
# include it again.
EMMC_OPTIONAL_FIELDS = ['prv']
def GetCategoryFromArgs(self):
return None
@classmethod
def ProbeAllDevices(cls):
return [ident for ident in map(cls._ProcessNode, GetFixedDevices())
if ident is not None]
@classmethod
def _ProcessNode(cls, node_path):
logging.info('Processing the node: %s', node_path)
dev_path = os.path.join(node_path, 'device')
# The directory layout for NVMe is "/<path>/device/device/<entries>"
nvme_dev_path = os.path.join(dev_path, 'device')
data = (sysfs.ReadSysfs(dev_path, cls.ATA_FIELDS) or
sysfs.ReadSysfs(nvme_dev_path, cls.NVME_FIELDS) or
sysfs.ReadSysfs(dev_path, cls.EMMC_FIELDS,
cls.EMMC_OPTIONAL_FIELDS))
if not data:
return None
fw_version = GetStorageFirmwareVersion(node_path)
if fw_version is not None:
data['fw_version'] = fw_version
size_path = os.path.join(os.path.dirname(dev_path), 'size')
data['sectors'] = (file_utils.ReadFile(size_path).strip()
if os.path.exists(size_path) else '')
return data