blob: 615b99a67d2142d29ccf04633d5411c815190cfb [file] [log] [blame]
import sys, os, re, string
from autotest_lib.client.bin import utils, fsinfo, fsdev_mgr, partition
from autotest_lib.client.common_lib import error
fd_mgr = fsdev_mgr.FsdevManager()
# For unmounting / formatting file systems we may have to use a device name
# that is different from the real device name that we have to use to set I/O
# scheduler tunables.
_DISKPART_FILE = '/proc/partitions'
def get_disk_list(std_mounts_only=True, get_all_disks=False):
"""
Get a list of dictionaries with information about disks on this system.
@param std_mounts_only: Whether the function should return only disks that
have a mount point defined (True) or even devices that doesn't
(False).
@param get_all_disks: Whether the function should return only partitioned
disks (False) or return every disk, regardless of being partitioned
or not (True).
@return: List of dictionaries with disk information (see more below).
The 'disk_list' array returned by get_disk_list() has an entry for each
disk drive we find on the box. Each of these entries is a map with the
following 3 string values:
'device' disk device name (i.e. the part after /dev/)
'mountpt' disk mount path
'tunable' disk name for setting scheduler tunables (/sys/block/sd??)
The last value is an integer that indicates the current mount status
of the drive:
'mounted' 0 = not currently mounted
1 = mounted r/w on the expected path
-1 = mounted readonly or at an unexpected path
When the 'std_mounts_only' argument is True we don't include drives
mounted on 'unusual' mount points in the result. If a given device is
partitioned, it will return all partitions that exist on it. If it's not,
it will return the device itself (ie, if there are /dev/sdb1 and /dev/sdb2,
those will be returned but not /dev/sdb. if there is only a /dev/sdc, that
one will be returned).
"""
# Get hold of the currently mounted file systems
mounts = utils.system_output('mount').splitlines()
# Grab all the interesting disk partition names from /proc/partitions,
# and build up the table of drives present in the system.
hd_list = []
# h for IDE drives, s for SATA/SCSI drives, v for Virtio drives
hd_regexp = re.compile("([hsv]d[a-z]+3)$")
partfile = open(_DISKPART_FILE)
for partline in partfile:
parts = partline.strip().split()
if len(parts) != 4 or partline.startswith('major'):
continue
# Get hold of the partition name
partname = parts[3]
if not get_all_disks:
# The partition name better end with a digit
# (get only partitioned disks)
if not partname[-1:].isdigit():
continue
# Process any site-specific filters on the partition name
if not fd_mgr.use_partition(partname):
continue
# We need to know the IDE/SATA/... device name for setting tunables
tunepath = fd_mgr.map_drive_name(partname)
# Check whether the device is mounted (and how)
mstat = 0
fstype = ''
fsopts = ''
fsmkfs = '?'
# Prepare the full device path for matching
chkdev = '/dev/' + partname
mountpt = None
for mln in mounts:
splt = mln.split()
# Typical 'mount' output line looks like this (indices
# for the split() result shown below):
#
# <device> on <mount_point> type <fstp> <options>
# 0 1 2 3 4 5
if splt[0].strip() == chkdev.strip():
# Make sure the mount point looks reasonable
mountpt = fd_mgr.check_mount_point(partname, splt[2])
if not mountpt:
mstat = -1
break
# Grab the file system type and mount options
fstype = splt[4]
fsopts = splt[5]
# Check for something other than a r/w mount
if fsopts[:3] != '(rw':
mstat = -1
break
# The drive is mounted at the 'normal' mount point
mstat = 1
# Does the caller only want to allow 'standard' mount points?
if std_mounts_only and mstat < 0:
continue
device_name = ''
if not get_all_disks:
# Was this partition mounted at all?
if not mountpt:
mountpt = fd_mgr.check_mount_point(partname, None)
# Ask the client where we should mount this partition
if not mountpt:
continue
else:
if partname[-1:].isdigit():
device_name = re.sub("\d", "", "/dev/%s" % partname)
if get_all_disks:
if not device_name:
continue
# Looks like we have a valid disk drive, add it to the list
hd_list.append({ 'device' : partname,
'mountpt': mountpt,
'tunable': tunepath,
'fs_type': fstype,
'fs_opts': fsopts,
'fs_mkfs': fsmkfs,
'mounted': mstat })
return hd_list
def mkfs_all_disks(job, disk_list, fs_type, fs_makeopt, fs_mnt_opt):
"""
Prepare all the drives in 'disk_list' for testing. For each disk this means
unmounting any mount points that use the disk, running mkfs with 'fs_type'
as the file system type and 'fs_makeopt' as the 'mkfs' options, and finally
remounting the freshly formatted drive using the flags in 'fs_mnt_opt'.
"""
for disk in disk_list:
# For now, ext4 isn't quite ready for prime time
if fs_type == "ext4":
fs_type = "ext4dev"
# Grab the device and mount paths for the drive
dev_path = os.path.join('/dev', disk["device"])
mnt_path = disk['mountpt']
# Create a file system instance
try:
fs = job.filesystem(device=dev_path, mountpoint=mnt_path)
except:
raise Exception("Could not create a filesystem on '%s'" % dev_path)
# Make sure the volume is unmounted
if disk["mounted"]:
try:
fs.unmount(mnt_path)
except Exception, info:
raise Exception("umount failed: exception = %s, args = %s" %
(sys.exc_info()[0], info.args))
except:
raise Exception("Could not unmount device ", dev_path)
# Is the drive already formatted with the right file system?
skip_mkfs = match_fs(disk, dev_path, fs_type, fs_makeopt)
# Next step is to create a fresh file system (if we need to)
try:
if not skip_mkfs:
fs.mkfs(fstype = fs_type, args = fs_makeopt)
except:
raise Exception("Could not 'mkfs " + "-t " + fs_type + " " +
fs_makeopt + " " + dev_path + "'")
# Mount the drive with the appropriate FS options
try:
opts = ""
if fs_mnt_opt != "":
opts += " -o " + fs_mnt_opt
fs.mount(mountpoint = mnt_path, fstype = fs_type, args = opts)
except NameError, info:
raise Exception("mount name error: %s" % info)
except Exception, info:
raise Exception("mount failed: exception = %s, args = %s" %
(type(info), info.args))
# If we skipped mkfs we need to wipe the partition clean
if skip_mkfs:
fs.wipe()
# Record the new file system type and options in the disk list
disk["mounted"] = True
disk["fs_type"] = fs_type
disk["fs_mkfs"] = fs_makeopt
disk["fs_opts"] = fs_mnt_opt
# Try to wipe the file system slate clean
utils.drop_caches()
# XXX(gps): Remove this code once refactoring is complete to get rid of these
# nasty test description strings.
def _legacy_str_to_test_flags(fs_desc_string):
"""Convert a legacy FS_LIST string into a partition.FsOptions instance."""
match = re.search('(.*?)/(.*?)/(.*?)/(.*)$', fs_desc_string.strip())
if not match:
raise ValueError('unrecognized FS list entry %r' % fs_desc_string)
flags_obj = partition.FsOptions(fstype=match.group(1).strip(),
mkfs_flags=match.group(2).strip(),
mount_options=match.group(3).strip(),
fs_tag=match.group(4).strip())
return flags_obj
def prepare_disks(job, fs_desc, disk1_only=False, disk_list=None):
"""
Prepare drive(s) to contain the file system type / options given in the
description line 'fs_desc'. When 'disk_list' is not None, we prepare all
the drives in that list; otherwise we pick the first available data drive
(which is usually hdc3) and prepare just that one drive.
Args:
fs_desc: A partition.FsOptions instance describing the test -OR- a
legacy string describing the same in '/' separated format:
'fstype / mkfs opts / mount opts / short name'.
disk1_only: Boolean, defaults to False. If True, only test the first
disk.
disk_list: A list of disks to prepare. If None is given we default to
asking get_disk_list().
Returns:
(mount path of the first disk, short name of the test, list of disks)
OR (None, '', None) if no fs_desc was given.
"""
# Special case - do nothing if caller passes no description.
if not fs_desc:
return (None, '', None)
if not isinstance(fs_desc, partition.FsOptions):
fs_desc = _legacy_str_to_test_flags(fs_desc)
# If no disk list was given, we'll get it ourselves
if not disk_list:
disk_list = get_disk_list()
# Make sure we have the appropriate 'mkfs' binary for the file system
mkfs_bin = 'mkfs.' + fs_desc.filesystem
if fs_desc.filesystem == 'ext4':
mkfs_bin = 'mkfs.ext4dev'
try:
utils.system('which ' + mkfs_bin)
except Exception:
try:
mkfs_bin = os.path.join(job.toolsdir, mkfs_bin)
utils.system('cp -ufp %s /sbin' % mkfs_bin)
except Exception:
raise error.TestError('No mkfs binary available for ' +
fs_desc.filesystem)
# For 'ext4' we need to add '-E test_fs' to the mkfs options
if fs_desc.filesystem == 'ext4':
fs_desc.mkfs_flags += ' -E test_fs'
# If the caller only needs one drive, grab the first one only
if disk1_only:
disk_list = disk_list[0:1]
# We have all the info we need to format the drives
mkfs_all_disks(job, disk_list, fs_desc.filesystem,
fs_desc.mkfs_flags, fs_desc.mount_options)
# Return(mount path of the first disk, test tag value, disk_list)
return (disk_list[0]['mountpt'], fs_desc.fs_tag, disk_list)
def restore_disks(job, restore=False, disk_list=None):
"""
Restore ext2 on the drives in 'disk_list' if 'restore' is True; when
disk_list is None, we do nothing.
"""
if restore and disk_list is not None:
prepare_disks(job, 'ext2 / -q -i20480 -m1 / / restore_ext2',
disk1_only=False,
disk_list=disk_list)
def wipe_disks(job, disk_list):
"""
Wipe all of the drives in 'disk_list' using the 'wipe' functionality
in the filesystem class.
"""
for disk in disk_list:
partition.wipe_filesystem(job, disk['mountpt'])
def match_fs(disk, dev_path, fs_type, fs_makeopt):
"""
Matches the user provided fs_type and fs_makeopt with the current disk.
"""
if disk["fs_type"] != fs_type:
return False
elif disk["fs_mkfs"] == fs_makeopt:
# No need to mkfs the volume, we only need to remount it
return True
elif fsinfo.match_mkfs_option(fs_type, dev_path, fs_makeopt):
if disk["fs_mkfs"] != '?':
raise Exception("mkfs option strings differ but auto-detection"
" code thinks they're identical")
else:
return True
else:
return False
##############################################################################
# The following variables/methods are used to invoke fsdev in 'library' mode
FSDEV_JOB = None
FSDEV_FS_DESC = None
FSDEV_RESTORE = None
FSDEV_PREP_CNT = 0
FSDEV_DISK1_ONLY = None
FSDEV_DISKLIST = None
def use_fsdev_lib(fs_desc, disk1_only, reinit_disks):
"""
Called from the control file to indicate that fsdev is to be used.
"""
global FSDEV_FS_DESC
global FSDEV_RESTORE
global FSDEV_DISK1_ONLY
global FSDEV_PREP_CNT
# This is a bit tacky - we simply save the arguments in global variables
FSDEV_FS_DESC = fs_desc
FSDEV_DISK1_ONLY = disk1_only
FSDEV_RESTORE = reinit_disks
# We need to keep track how many times 'prepare' is called
FSDEV_PREP_CNT = 0
def prepare_fsdev(job):
"""
Called from the test file to get the necessary drive(s) ready; return
a pair of values: the absolute path to the first drive's mount point
plus the complete disk list (which is useful for tests that need to
use more than one drive).
"""
global FSDEV_JOB
global FSDEV_DISKLIST
global FSDEV_PREP_CNT
if not FSDEV_FS_DESC:
return (None, None)
# Avoid preparing the same thing more than once
FSDEV_PREP_CNT += 1
if FSDEV_PREP_CNT > 1:
return (FSDEV_DISKLIST[0]['mountpt'],FSDEV_DISKLIST)
FSDEV_JOB = job
(path,toss,disks) = prepare_disks(job, fs_desc = FSDEV_FS_DESC,
disk1_only = FSDEV_DISK1_ONLY,
disk_list = None)
FSDEV_DISKLIST = disks
return (path,disks)
def finish_fsdev(force_cleanup=False):
"""
This method can be called from the test file to optionally restore
all the drives used by the test to a standard ext2 format. Note that
if use_fsdev_lib() was invoked with 'reinit_disks' not set to True,
this method does nothing. Note also that only fsdev "server-side"
dynamic control files should ever set force_cleanup to True.
"""
if FSDEV_PREP_CNT == 1 or force_cleanup:
restore_disks(job = FSDEV_JOB,
restore = FSDEV_RESTORE,
disk_list = FSDEV_DISKLIST)
##############################################################################
class fsdev_disks:
"""
Disk drive handling class used for file system development
"""
def __init__(self, job):
self.job = job
# Some clients need to access the 'fsdev manager' instance directly
def get_fsdev_mgr(self):
return fd_mgr
def config_sched_tunables(self, desc_file):
# Parse the file that describes the scheduler tunables and their paths
self.tune_loc = eval(open(desc_file).read())
# Figure out what kernel we're running on
kver = utils.system_output('uname -r')
kver = re.match("([0-9]+\.[0-9]+\.[0-9]+).*", kver)
kver = kver.group(1)
# Make sure we know how to handle the kernel we're running on
tune_files = self.tune_loc[kver]
if tune_files is None:
raise Exception("Scheduler tunables not available for kernel " +
kver)
# Save the kernel version for later
self.kernel_ver = kver
# For now we always use 'anticipatory'
tune_paths = tune_files["anticipatory"]
# Create a dictionary out of the tunables array
self.tune_loc = {}
for tx in range(len(tune_paths)):
# Grab the next tunable path from the array
tpath = tune_paths[tx]
# Strip any leading directory names
tuner = tpath
while 1:
slash = tuner.find("/")
if slash < 0:
break
tuner = tuner[slash+1:]
# Add mapping to the dictionary
self.tune_loc[tuner] = tpath
def load_sched_tunable_values(self, val_file):
# Prepare the array of tunable values
self.tune_list = []
# Read the config parameters and find the values that match our kernel
for cfgline in open(val_file):
cfgline = cfgline.strip()
if len(cfgline) == 0:
continue
if cfgline.startswith("#"):
continue
if cfgline.startswith("tune[") == 0:
raise Exception("Config entry not recognized: " + cfgline)
endKV = cfgline.find("]:")
if endKV < 0:
raise Exception("Config entry missing closing bracket: "
+ cfgline)
if cfgline[5:endKV] != self.kernel_ver[0:endKV-5]:
continue
tune_parm = cfgline[endKV+2:].strip()
equal = tune_parm.find("=")
if equal < 1 or equal == len(tune_parm) - 1:
raise Exception("Config entry doesn't have 'parameter=value' :"
+ cfgline)
tune_name = tune_parm[:equal]
tune_val = tune_parm[equal+1:]
# See if we have a matching entry in the path dictionary
try:
tune_path = self.tune_loc[tune_name]
except:
raise Exception("Unknown config entry: " + cfgline)
self.tune_list.append((tune_name, tune_path, tune_val))
def set_sched_tunables(self, disks):
"""
Given a list of disks in the format returned by get_disk_list() above,
set the I/O scheduler values on all the disks to the values loaded
earlier by load_sched_tunables().
"""
for dx in range(len(disks)):
disk = disks[dx]['tunable']
# Set the scheduler first before setting any other tunables
self.set_tunable(disk, "scheduler",
self.tune_loc["scheduler"],
"anticipatory")
# Now set all the tunable parameters we've been given
for tune_desc in self.tune_list:
self.set_tunable(disk, tune_desc[0],
tune_desc[1],
tune_desc[2])
def set_tunable(self, disk, name, path, val):
"""
Given a disk name, a path to a tunable value under _TUNE_PATH and the
new value for the parameter, set the value and verify that the value
has been successfully set.
"""
fpath = partition.get_iosched_path(disk, path)
# Things might go wrong so we'll catch exceptions
try:
step = "open tunable path"
tunef = open(fpath, 'w', buffering=-1)
step = "write new tunable value"
tunef.write(val)
step = "close the tunable path"
tunef.close()
step = "read back new tunable value"
nval = open(fpath, 'r', buffering=-1).read().strip()
# For 'scheduler' we need to fish out the bracketed value
if name == "scheduler":
nval = re.match(".*\[(.*)\].*", nval).group(1)
except IOError, info:
# Special case: for some reason 'max_sectors_kb' often doesn't work
# with large values; try '128' if we haven't tried it already.
if name == "max_sectors_kb" and info.errno == 22 and val != '128':
self.set_tunable(disk, name, path, '128')
return;
# Something went wrong, probably a 'config' problem of some kind
raise Exception("Unable to set tunable value '" + name +
"' at step '" + step + "': " + str(info))
except Exception:
# We should only ever see 'IOError' above, but just in case ...
raise Exception("Unable to set tunable value for " + name)
# Make sure the new value is what we expected
if nval != val:
raise Exception("Unable to correctly set tunable value for "
+ name +": desired " + val + ", but found " + nval)
return