bareon/bareon/utils/hardware.py

491 lines
15 KiB
Python

#
# Copyright 2017 Cray Inc. All Rights Reserved.
#
# 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 errno
import os
import re
import stat
from oslo_log import log as logging
from bareon import errors
from bareon.utils import utils
LOG = logging.getLogger(__name__)
# Please take a look at the linux kernel documentation
# https://github.com/torvalds/linux/blob/master/Documentation/devices.txt.
# KVM virtio volumes have major number 252 in CentOS, but 253 in Ubuntu.
# NOTE(agordeev): nvme devices also have a major number of 259
# (only in 2.6 kernels)
# KVM virtio volumes have major number 254 in Debian
VALID_MAJORS = (3, 8, 9, 65, 66, 67, 68, 69, 70, 71, 104, 105, 106, 107, 108,
109, 110, 111, 202, 251, 252, 253, 254, 259)
# We are only interested in getting these
# properties from udevadm report
# MAJOR major device number
# MINOR minor device number
# DEVNAME e.g. /dev/sda
# DEVTYPE e.g. disk or partition for block devices
# DEVPATH path to a device directory relative to /sys
# ID_BUS e.g. ata, scsi
# ID_MODEL e.g. MATSHITADVD-RAM_UJ890
# ID_SERIAL_SHORT e.g. UH00_296679
# ID_WWN e.g. 0x50000392e9804d4b (optional)
# ID_CDROM e.g. 1 for cdrom device (optional)
# DM_UUID e.g. mpath-3600144f0534f392c000056e972830002 for devices, mapped by
# device mapper (optional)
UDEV_PROPERTIES = set(['MAJOR', 'MINOR', 'DEVNAME', 'DEVTYPE', 'DEVPATH',
'ID_BUS', 'ID_MODEL', 'ID_SERIAL_SHORT', 'ID_WWN',
'ID_CDROM', 'ID_VENDOR', 'DM_UUID'])
# more details about types you can find in dmidecode's manual
SMBIOS_TYPES = {'bios': '0',
'base_board': '2',
'processor': '4',
'memory_array': '16',
'memory_device': '17'}
# Device types
DISK = 'disk'
PARTITION = 'partition'
def parse_dmidecode(type):
"""Parses `dmidecode` output.
:param type: A string with type of entity to display.
:returns: A list with dictionaries of entities for specified type.
"""
output = utils.execute('dmidecode', '-q', '--type', type)
lines = output[0].split('\n')
info = []
multiline_values = None
section = 0
for line in lines:
if len(line) != 0 and len(line.strip()) == len(line):
info.append({})
section = len(info) - 1
try:
k, v = (l.strip() for l in line.split(':', 1))
except ValueError:
k = line.strip()
if not k:
multiline_values = None
if multiline_values:
info[section][multiline_values].append(k)
else:
if not v:
multiline_values = k.lower()
info[section][multiline_values] = []
else:
info[section][k.lower()] = v
return info
def parse_lspci():
"""Parses `lspci` output.
:returns: A list of dicts containing PCI devices information
"""
output = utils.execute('lspci', '-vmm', '-D')
lines = output[0].split('\n')
info = [{}]
section = 0
for line in lines[:-2]:
try:
k, v = (l.strip() for l in line.split(':', 1))
except ValueError:
info.append({})
section += 1
else:
info[section][k.lower()] = v
return info
def parse_simple_kv(*command):
"""Parses simple key:value output from specified command.
:param command: A command to execute
:returns: A dict of parsed key-value data
"""
output = utils.execute(*command)
lines = output[0].split('\n')
info = {}
for line in lines[:-1]:
try:
k, v = (l.strip() for l in line.split(':', 1))
except ValueError:
break
else:
info[k.lower()] = v
return info
def is_disk(dev, bspec=None, uspec=None):
"""Checks if given device is a disk.
:param dev: A device file, e.g. /dev/sda.
:param bspec: A dict of properties which we get from blockdev.
:param uspec: A dict of properties which we get from udevadm.
:returns: True if device is disk else False.
"""
# Filtering by udevspec
if uspec is None:
uspec = udevreport(dev)
if uspec.get('ID_CDROM') == '1':
return False
if uspec.get('DEVTYPE') == 'partition':
return False
if 'MAJOR' in uspec and int(uspec['MAJOR']) not in VALID_MAJORS:
return False
# Filtering by blockdev spec
if bspec is None:
bspec = blockdevreport(dev)
if bspec.get('ro') == '1':
return False
return True
def udevreport(dev):
"""Builds device udevadm report.
:param dev: A device file, e.g. /dev/sda.
:returns: A dict of udev device properties.
"""
report = utils.execute('udevadm',
'info',
'--query=property',
'--export',
'--name={0}'.format(dev),
check_exit_code=[0])[0]
spec = {}
for line in [l for l in report.splitlines() if l]:
key, value = line.split('=', 1)
value = value.strip('\'')
# This is a list of symbolic links which were created for this
# block device (e.g. /dev/disk/by-id/foobar)
if key == 'DEVLINKS':
spec['DEVLINKS'] = value.split()
if key in UDEV_PROPERTIES:
spec[key] = value
return spec
def blockdevreport(blockdev):
"""Builds device blockdev report.
:param blockdev: A block device file, e.g. /dev/sda.
:returns: A dict of blockdev properties.
"""
cmd = [
'blockdev',
'--getsz', # get size in 512-byte sectors
'--getro', # get read-only
'--getss', # get logical block (sector) size
'--getpbsz', # get physical block (sector) size
'--getsize64', # get size in bytes
'--getiomin', # get minimum I/O size
'--getioopt', # get optimal I/O size
'--getra', # get readahead
'--getalignoff', # get alignment offset in bytes
'--getmaxsect', # get max sectors per request
blockdev
]
opts = [o[5:] for o in cmd if o.startswith('--get')]
report = utils.execute(*cmd, check_exit_code=[0])[0]
return dict(zip(opts, report.splitlines()))
def extrareport(dev):
"""Builds device report using some additional sources.
:param dev: A device file, e.g. /dev/sda.
:returns: A dict of properties.
"""
spec = {}
name = os.path.basename(dev)
# Finding out if block device is removable or not
# actually, some disks are marked as removable
# while they are actually not e.g. Adaptec RAID volumes
try:
with open('/sys/block/{0}/removable'.format(name)) as file:
spec['removable'] = file.read().strip()
except Exception:
pass
for key in ('state', 'timeout', 'vendor'):
try:
with open('/sys/block/{0}/device/{1}'.format(name, key)) as file:
spec[key] = file.read().strip()
except Exception:
pass
return spec
def dev_to_scsi_map():
dev_to_scsi = {}
sys_path = '/sys/class/scsi_device'
block_device_mapping = 'device/block'
for scsi_addr in os.listdir(sys_path):
mapping_path = os.path.join(sys_path, scsi_addr, block_device_mapping)
try:
for block_devices in os.listdir(mapping_path):
block_devices = os.path.join('/dev', block_devices)
dev_to_scsi[block_devices] = scsi_addr
except OSError as e:
if e.errno != errno.ENOENT:
raise
return dev_to_scsi
def is_block_device(filepath):
"""Check whether `filepath` is a block device."""
mode = os.stat(filepath).st_mode
return stat.S_ISBLK(mode)
def is_multipath_device(device, uspec=None):
"""Check whether block device with given uspec is multipath device"""
if uspec is None:
uspec = udevreport(device)
# NOTE(sslypushenko)DM_UUID is converted to uppercase to be sure, that
# multipath devices will be found correctly
return uspec.get('DM_UUID', '').upper().startswith('MPATH')
def get_block_devices_from_udev_db():
return get_block_data_from_udev('disk')
def get_partitions_from_udev_db():
return get_block_data_from_udev('partition')
def get_vg_devices_from_udev_db():
return get_block_data_from_udev('disk', vg=True)
def _is_valid_dev_type(device_info, vg):
"""Returns bool value if we should use device based on different rules:
1. Should have approved MAJOR number
2. Shouldn't be nbd/ram/loop device
3. Should contain DEVNAME itself
4. Should be compared with vg value
:param device_info: A dict of properties which we get from udevadm.
:param vg: determine if we need LVM devices or not.
:returns: bool if we should use this device.
"""
if (
'E: MAJOR' in device_info and
int(device_info['E: MAJOR']) not in VALID_MAJORS
):
# NOTE(agordeev): filter out cd/dvd drives and other
# block devices in which bareon aren't interested
return False
if any(
os.path.basename(device_info['E: DEVNAME']).startswith(n)
for n in ('nbd', 'ram', 'loop')
):
return False
if 'E: DEVNAME' not in device_info:
return False
if (vg and 'E: DM_VG_NAME' in device_info or
not vg and 'E: DM_VG_NAME' not in device_info):
return True
else:
return False
def get_block_data_from_udev(devtype, vg=False):
devs = []
output = utils.execute('udevadm', 'info', '--export-db')[0]
for device in output.split('\n\n'):
# NOTE(agordeev): add only disks or their partitions
if 'SUBSYSTEM=block' in device and 'DEVTYPE=%s' % devtype in device:
# python 2.6 do not support dict comprehension
device_info = dict((line.partition('=')[0], line.partition('=')[2])
for line in device.split('\n')
if line.startswith('E:'))
if _is_valid_dev_type(device_info, vg):
devs.append(device_info['E: DEVNAME'])
return devs
def list_block_devices(disks=True):
"""Gets list of block devices
Tries to guess which of them are disks
and returns list of dicts representing those disks.
:returns: A list of dict representing disks available on a node.
"""
bdevs = []
# NOTE(agordeev): blockdev from util-linux contains a bug
# which's causing 'blockdev --report' to fail on reporting
# nvme devices.
# The actual fix is included in util-linux-2.24.1:
# - don't use HDIO_GETGEO [Phillip Susi]
# Since the bug only affects '--report' it is safe to use
# 'blockdevreport'.
# bareon has to be switched to use udev database in order to
# find all block devices recognized by kernel.
devs = get_block_devices_from_udev_db()
for device in devs:
bdev = get_device_info(device, disks)
if bdev:
bdevs.append(bdev)
return bdevs
def get_device_ids(device):
uspec = udevreport(device)
if 'DEVLINKS' not in uspec:
return None
paths = []
for element in uspec['DEVLINKS']:
regex = re.search(r'disk/by.*', element)
if regex:
val = regex.group(0)
paths.append(val)
return {'name': device, 'paths': paths}
def get_device_info(device, disks=True):
try:
uspec = udevreport(device)
espec = extrareport(device)
bspec = blockdevreport(device)
except (KeyError, ValueError, TypeError,
errors.ProcessExecutionError) as e:
LOG.warning('Skipping block device %s. '
'Failed to get all information about the device: %s',
device, e)
return
# if device is not disk, skip it
if disks and not is_disk(device, bspec=bspec, uspec=uspec):
return
# NOTE(kszukielojc) if block device is multipath device,
# devlink /dev/mapper/* should be used instead /dev/dm-*
if is_multipath_device(device, uspec=uspec):
device = [devlink for devlink in uspec['DEVLINKS']
if devlink.startswith('/dev/mapper/')][0]
bdev = {
'device': device,
# NOTE(agordeev): blockdev gets 'startsec' from sysfs,
# 'size' is determined by ioctl call.
# This data was not actually used by bareon,
# so it can be removed without side effects.
'uspec': uspec,
'bspec': bspec,
'espec': espec
}
return bdev
def match_device(uspec1, uspec2):
"""Tries to find out if uspec1 and uspec2 are uspecs from the same device
It compares only some fields in uspecs (not all of them) which, we believe,
is enough to say exactly whether uspecs belong to the same device or not.
:param uspec1: A dict of properties which we get from udevadm.
:param uspec1: A dict of properties which we get from udevadm.
:returns: True if uspecs match each other else False.
"""
# False if ID_WWN is given and does not match each other
if ('ID_WWN' in uspec1 and 'ID_WWN' in uspec2
and uspec1['ID_WWN'] != uspec2['ID_WWN']):
return False
# False if ID_SERIAL_SHORT is given and does not match each other
if ('ID_SERIAL_SHORT' in uspec1 and 'ID_SERIAL_SHORT' in uspec2
and uspec1['ID_SERIAL_SHORT'] != uspec2['ID_SERIAL_SHORT']):
return False
# True if at least one by-id link is the same for both uspecs
if ('DEVLINKS' in uspec1 and 'DEVLINKS' in uspec2
and any(x.startswith('/dev/disk/by-id') for x in
set(uspec1['DEVLINKS']) & set(uspec2['DEVLINKS']))):
return True
# True if ID_WWN is given and matches each other
# and DEVTYPE is given and is 'disk'
if (uspec1.get('ID_WWN') == uspec2.get('ID_WWN') is not None
and uspec1.get('DEVTYPE') == uspec2.get('DEVTYPE') == 'disk'):
return True
# True if ID_WWN is given and matches each other
# and DEVTYPE is given and is 'partition'
# and MINOR is given and matches each other
if (uspec1.get('ID_WWN') == uspec2.get('ID_WWN') is not None
and uspec1.get('DEVTYPE') == uspec2.get('DEVTYPE') == 'partition'
and uspec1.get('MINOR') == uspec2.get('MINOR') is not None):
return True
# True if ID_SERIAL_SHORT is given and matches each other
# and DEVTYPE is given and is 'disk'
if (uspec1.get('ID_SERIAL_SHORT') == uspec2.get('ID_SERIAL_SHORT')
is not None
and uspec1.get('DEVTYPE') == uspec2.get('DEVTYPE') == 'disk'):
return True
# True if DEVPATH is given and matches each other
if uspec1.get('DEVPATH') == uspec2.get('DEVPATH') is not None:
return True
return False