metal/inventory/inventory/inventory/agent/disk.py

370 lines
14 KiB
Python

#
# Copyright (c) 2013-2018 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# All Rights Reserved.
#
""" inventory idisk Utilities and helper functions."""
import os
import pyudev
import re
import subprocess
import sys
from inventory.common import constants
from inventory.common import context
from inventory.common import utils
from inventory.conductor import rpcapi as conductor_rpcapi
from oslo_log import log
LOG = log.getLogger(__name__)
class DiskOperator(object):
'''Class to encapsulate Disk operations for System Inventory'''
def __init__(self):
self.num_cpus = 0
self.num_nodes = 0
self.float_cpuset = 0
self.default_hugepage_size_kB = 0
self.total_memory_MiB = 0
self.free_memory_MiB = 0
self.total_memory_nodes_MiB = []
self.free_memory_nodes_MiB = []
self.topology = {}
def convert_range_string_to_list(self, s):
olist = []
s = s.strip()
if s:
for part in s.split(','):
if '-' in part:
a, b = part.split('-')
a, b = int(a), int(b)
olist.extend(range(a, b + 1))
else:
a = int(part)
olist.append(a)
olist.sort()
return olist
def get_rootfs_node(self):
cmdline_file = '/proc/cmdline'
device = None
with open(cmdline_file, 'r') as f:
for line in f:
for param in line.split():
params = param.split("=", 1)
if params[0] == "root":
if "UUID=" in params[1]:
key, uuid = params[1].split("=")
symlink = "/dev/disk/by-uuid/%s" % uuid
device = os.path.basename(os.readlink(symlink))
else:
device = os.path.basename(params[1])
if device is not None:
if constants.DEVICE_NAME_NVME in device:
re_line = re.compile(r'^(nvme[0-9]*n[0-9]*)')
else:
re_line = re.compile(r'^(\D*)')
match = re_line.search(device)
if match:
return os.path.join("/dev", match.group(1))
return
@utils.skip_udev_partition_probe
def get_disk_available_mib(self, device_node):
# Check that partition table format is GPT.
# Return 0 if not.
if not utils.disk_is_gpt(device_node=device_node):
LOG.debug("Format of disk node %s is not GPT." % device_node)
return 0
pvs_command = '{} {}'.format('pvs | grep -w ', device_node)
pvs_process = subprocess.Popen(pvs_command, stdout=subprocess.PIPE,
shell=True)
pvs_output = pvs_process.stdout.read()
if pvs_output:
LOG.debug("Disk %s is completely used by a PV => 0 available mib."
% device_node)
return 0
# Get sector size command.
sector_size_bytes_cmd = '{} {}'.format('blockdev --getss', device_node)
# Get total free space in sectors command.
avail_space_sectors_cmd = '{} {} {}'.format(
'sgdisk -p', device_node, "| grep \"Total free space\"")
# Get the sector size.
sector_size_bytes_process = subprocess.Popen(
sector_size_bytes_cmd, stdout=subprocess.PIPE, shell=True)
sector_size_bytes = sector_size_bytes_process.stdout.read().rstrip()
# Get the free space.
avail_space_sectors_process = subprocess.Popen(
avail_space_sectors_cmd, stdout=subprocess.PIPE, shell=True)
avail_space_sectors_output = avail_space_sectors_process.stdout.read()
avail_space_sectors = re.findall(
'\d+', avail_space_sectors_output)[0].rstrip()
# Free space in MiB.
avail_space_mib = (int(sector_size_bytes) * int(avail_space_sectors) /
(1024 ** 2))
# Keep 2 MiB for partition table.
if avail_space_mib >= 2:
avail_space_mib = avail_space_mib - 2
else:
avail_space_mib = 0
return avail_space_mib
def disk_format_gpt(self, host_uuid, idisk_dict, is_cinder_device):
disk_node = idisk_dict.get('device_path')
utils.disk_wipe(disk_node)
utils.execute('parted', disk_node, 'mklabel', 'gpt')
if is_cinder_device:
LOG.debug("Removing .node_cinder_lvm_config_complete_file")
try:
os.remove(constants.NODE_CINDER_LVM_CONFIG_COMPLETE_FILE)
except OSError:
LOG.error(".node_cinder_lvm_config_complete_file not present.")
pass
# On SX ensure wipe succeeds before DB is updated.
# Flag file is used to mark wiping in progress.
try:
os.remove(constants.DISK_WIPE_IN_PROGRESS_FLAG)
except OSError:
# it's ok if file is not present.
pass
# We need to send the updated info about the host disks back to
# the conductor.
idisk_update = self.idisk_get()
ctxt = context.get_admin_context()
rpcapi = conductor_rpcapi.ConductorAPI(
topic=conductor_rpcapi.MANAGER_TOPIC)
rpcapi.idisk_update_by_ihost(ctxt,
host_uuid,
idisk_update)
def handle_exception(self, e):
traceback = sys.exc_info()[-1]
LOG.error("%s @ %s:%s" % (
e, traceback.tb_frame.f_code.co_filename, traceback.tb_lineno))
def is_rotational(self, device_name):
"""Find out if a certain disk is rotational or not. Mostly used for
determining if disk is HDD or SSD.
"""
# Obtain the path to the rotational file for the current device.
device = device_name['DEVNAME'].split('/')[-1]
rotational_path = "/sys/block/{device}/queue/rotational"\
.format(device=device)
rotational = None
# Read file and remove trailing whitespaces.
if os.path.isfile(rotational_path):
with open(rotational_path, 'r') as rot_file:
rotational = rot_file.read()
rotational = rotational.rstrip()
return rotational
def get_device_id_wwn(self, device):
"""Determine the ID and WWN of a disk from the value of the DEVLINKS
attribute.
Note: This data is not currently being used for anything. We are
gathering this information so conductor can store for future use.
"""
# The ID and WWN default to None.
device_id = None
device_wwn = None
# If there is no DEVLINKS attribute, return None.
if 'DEVLINKS' not in device:
return device_id, device_wwn
# Extract the ID and the WWN.
LOG.debug("[DiskEnum] get_device_id_wwn: devlinks= %s" %
device['DEVLINKS'])
devlinks = device['DEVLINKS'].split()
for devlink in devlinks:
if "by-id" in devlink:
if "wwn" not in devlink:
device_id = devlink.split('/')[-1]
LOG.debug("[DiskEnum] by-id: %s id: %s" % (devlink,
device_id))
else:
device_wwn = devlink.split('/')[-1]
LOG.debug("[DiskEnum] by-wwn: %s wwn: %s" % (devlink,
device_wwn))
return device_id, device_wwn
def idisk_get(self):
"""Enumerate disk topology based on:
:param self
:returns list of disk and attributes
"""
idisk = []
context = pyudev.Context()
for device in context.list_devices(DEVTYPE='disk'):
if not utils.is_system_usable_block_device(device):
continue
if device['MAJOR'] in constants.VALID_MAJOR_LIST:
if 'ID_PATH' in device:
device_path = "/dev/disk/by-path/" + device['ID_PATH']
LOG.debug("[DiskEnum] device_path: %s ", device_path)
else:
# We should always have a udev supplied /dev/disk/by-path
# value as a matter of normal operation. We do not expect
# this to occur, thus the error.
#
# The kickstart files for the host install require the
# by-path value also to be present or the host install will
# fail. Since the installer and the runtime share the same
# kernel/udev we should not see this message on an
# installed system.
device_path = None
LOG.error("Device %s does not have an ID_PATH value "
"provided by udev" % device.device_node)
size_mib = 0
available_mib = 0
model_num = ''
serial_id = ''
# Can merge all try/except in one block but this allows
# at least attributes with no exception to be filled
try:
size_mib = utils.get_disk_capacity_mib(device.device_node)
except Exception as e:
self.handle_exception("Could not retrieve disk size - %s "
% e)
try:
available_mib = self.get_disk_available_mib(
device_node=device.device_node)
except Exception as e:
self.handle_exception(
"Could not retrieve disk %s free space" % e)
try:
# ID_MODEL received from udev is not correct for disks that
# are used entirely for LVM. LVM replaced the model ID with
# its own identifier that starts with "LVM PV".For this
# reason we will attempt to retrieve the correct model ID
# by using 2 different commands: hdparm and lsblk and
# hdparm. If one of them fails, the other one can attempt
# to retrieve the information. Else we use udev.
# try hdparm command first
hdparm_command = 'hdparm -I %s |grep Model' % (
device.get('DEVNAME'))
hdparm_process = subprocess.Popen(
hdparm_command,
stdout=subprocess.PIPE,
shell=True)
hdparm_output = hdparm_process.communicate()[0]
if hdparm_process.returncode == 0:
second_half = hdparm_output.split(':')[1]
model_num = second_half.strip()
else:
# try lsblk command
lsblk_command = 'lsblk -dn --output MODEL %s' % (
device.get('DEVNAME'))
lsblk_process = subprocess.Popen(
lsblk_command,
stdout=subprocess.PIPE,
shell=True)
lsblk_output = lsblk_process.communicate()[0]
if lsblk_process.returncode == 0:
model_num = lsblk_output.strip()
else:
# both hdparm and lsblk commands failed, try udev
model_num = device.get('ID_MODEL')
if not model_num:
model_num = constants.DEVICE_MODEL_UNKNOWN
except Exception as e:
self.handle_exception("Could not retrieve disk model "
"for disk %s. Exception: %s" %
(device.get('DEVNAME'), e))
try:
if 'ID_SCSI_SERIAL' in device:
serial_id = device['ID_SCSI_SERIAL']
else:
serial_id = device['ID_SERIAL_SHORT']
except Exception as e:
self.handle_exception("Could not retrieve disk "
"serial ID - %s " % e)
capabilities = dict()
if model_num:
capabilities.update({'model_num': model_num})
if self.get_rootfs_node() == device.device_node:
capabilities.update({'stor_function': 'rootfs'})
rotational = self.is_rotational(device)
device_type = device.device_type
rotation_rate = constants.DEVICE_TYPE_UNDETERMINED
if rotational is '1':
device_type = constants.DEVICE_TYPE_HDD
if 'ID_ATA_ROTATION_RATE_RPM' in device:
rotation_rate = device['ID_ATA_ROTATION_RATE_RPM']
elif rotational is '0':
if constants.DEVICE_NAME_NVME in device.device_node:
device_type = constants.DEVICE_TYPE_NVME
else:
device_type = constants.DEVICE_TYPE_SSD
rotation_rate = constants.DEVICE_TYPE_NA
# TODO(sc) else: what are other possible stor_function value?
# or do we just use pair { 'is_rootfs': True } instead?
# Obtain device ID and WWN.
device_id, device_wwn = self.get_device_id_wwn(device)
attr = {
'device_node': device.device_node,
'device_num': device.device_number,
'device_type': device_type,
'device_path': device_path,
'device_id': device_id,
'device_wwn': device_wwn,
'size_mib': size_mib,
'available_mib': available_mib,
'serial_id': serial_id,
'capabilities': capabilities,
'rpm': rotation_rate,
}
idisk.append(attr)
LOG.debug("idisk= %s" % idisk)
return idisk