device-mapper-persistent-data/tests/functions_test/dmpd_library.py

2372 lines
82 KiB
Python
Executable File

#!/usr/bin/python
# Copyright (c) 2017 Red Hat, Inc. All rights reserved. This copyrighted material
# is made available to anyone wishing to use, modify, copy, or
# redistribute it subject to the terms and conditions of the GNU General
# Public License v.2.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see http://www.gnu.org/licenses/.
#
# Author: Jakub Krysl <jkrysl@redhat.com>
"""dmpd_library.py: Complete library providing functionality for device-mapper-persistent-data upstream test."""
import platform
from os.path import expanduser
import re #regex
import sys, os
import subprocess
import time
import fileinput
# TODO: Is this really necessary? Unlikely we will run into python2 in rawhide
# again...
from __future__ import print_function
def _print(string):
module_name = __name__
string = re.sub("DEBUG:", "DEBUG:("+ module_name + ") ", string)
string = re.sub("FAIL:", "FAIL:("+ module_name + ") ", string)
string = re.sub("FATAL:", "FATAL:("+ module_name + ") ", string)
string = re.sub("WARN:", "WARN:("+ module_name + ") ", string)
print(string)
return
def run(cmd, return_output=False, verbose=True, force_flush=False):
"""Run a command line specified as cmd.
The arguments are:
\tcmd (str): Command to be executed
\tverbose: if we should show command output or not
\tforce_flush: if we want to show command output while command is being executed. eg. hba_test run
\treturn_output (Boolean): Set to True if want output result to be returned as tuple. Default is False
Returns:
\tint: Return code of the command executed
\tstr: As tuple of return code if return_output is set to True
"""
#by default print command output
if (verbose == True):
#Append time information to command
date = "date \"+%Y-%m-%d %H:%M:%S\""
p = subprocess.Popen(date, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
stdout, stderr = p.communicate()
stdout = stdout.rstrip("\n")
_print("INFO: [%s] Running: '%s'..." % (stdout, cmd))
#enabling shell=True, because was the only way I found to run command with '|'
if not force_flush:
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
stdout, stderr = p.communicate()
sys.stdout.flush()
sys.stderr.flush()
else:
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True)
stdout = ""
stderr = ""
while p.poll() is None:
new_data = p.stdout.readline()
stdout += new_data
if verbose:
sys.stdout.write(new_data)
sys.stdout.flush()
retcode = p.returncode
output = stdout + stderr
#remove new line from last line
output = output.rstrip()
#by default print command output
#if force_flush we already printed it
if verbose == True and not force_flush:
print(output)
if return_output == False:
return retcode
else:
return retcode, output
def atomic_run(message, success=True, return_output=False, **kwargs):
errors = kwargs.pop("errors")
command = kwargs.pop("command")
params = []
for a in kwargs:
params.append(str(a) + " = " + str(kwargs[a]))
params = ", ".join([str(i) for i in params])
_print("\nINFO: " + message + " with params %s" % params)
if return_output:
kwargs["return_output"] = True
ret, output = command(**kwargs)
else:
ret = command(**kwargs)
expected_break = {True: False, False: True}
print("(Returned, Expected)")
if command == run:
expected_break = {True: 1, False: 0}
if ret in expected_break:
print(not expected_break[ret], success)
else:
print(ret, success)
else:
print(ret, success)
if ret == expected_break[success]:
error = "FAIL: " + message + " with params %s failed" % params
_print(error)
errors.append(error)
sleep(0.2)
if return_output:
return output
else:
return ret
def sleep(duration):
"""
It basically call sys.sleep, but as stdout and stderr can be buffered
We flush them before sleep
"""
sys.stdout.flush()
sys.stderr.flush()
time.sleep(duration)
return
def mkdir(new_dir):
if os.path.isdir(new_dir):
_print("INFO: %s already exist" % new_dir)
return True
cmd = "mkdir -p %s" % new_dir
retcode, output = run(cmd, return_output=True, verbose=False)
if retcode != 0:
_print("WARN: could create directory %s" % new_dir)
print(output)
return False
return True
def dist_release():
"""
Find out the release number of Linux distribution.
"""
dist = platform.linux_distribution()
if not dist or dist[1] == "":
_print("WARN: dist_release() - Could not determine dist release")
return None
return dist[1]
def dist_ver():
"""
Check the Linux distribution version.
"""
release = dist_release()
if not release:
return None
m = re.match("(\d+).\d+", release)
if m:
return int(m.group(1))
# See if it is only digits, in that case return it
m = re.match("(\d+)", release)
if m:
return int(m.group(1))
_print("WARN: dist_ver() - Invalid release output %s" % release)
return None
def show_sys_info():
print("### Kernel Info: ###")
ret, kernel = run ("uname -a", return_output=True, verbose=False)
ret, taint_val = run("cat /proc/sys/kernel/tainted", return_output=True, verbose=False)
print("Kernel version: %s" % kernel)
print("Kernel tainted: %s" % taint_val)
print("### IP settings: ###")
run("ip a")
if run("rpm -q device-mapper-multipath") == 0:
#Abort test execution if multipath is not working well
if run("multipath -l 2>/dev/null") != 0:
sys.exit(1)
#Flush all unused multipath devices before starting the test
run("multipath -F")
run("multipath -r")
def get_free_space(path):
"""
Get free space of a path.
Path could be:
\t/dev/sda
\t/root
\t./
"""
if not path:
return None
cmd = "df -B 1 %s" % (path)
retcode, output = run(cmd, return_output=True, verbose=False)
if retcode != 0:
_print("WARN: get_free_space() - could not run %s" % (cmd))
print(output)
return None
fs_list = output.split("\n")
# delete the header info
del fs_list[0]
if len(fs_list) > 1:
#Could be the information was too long and splited in lines
tmp_info = "".join(fs_list)
fs_list[0] = tmp_info
#expected order
#Filesystem 1B-blocks Used Available Use% Mounted on
free_space_regex = re.compile("\S+\s+\d+\s+\d+\s+(\d+)")
m = free_space_regex.search(fs_list[0])
if m:
return int(m.group(1))
return None
def size_human_2_size_bytes(size_human):
"""
Usage
size_human_2_size_bytes(size_human)
Purpose
Convert human readable stander size to B
Parameter
size_human # like '1KiB'
Returns
size_bytes # like 1024
"""
if not size_human:
return None
# make sure size_human is a string, could be only numbers, for example
size_human = str(size_human)
if not re.search("\d", size_human):
# Need at least 1 digit
return None
size_human_regex = re.compile("([\-0-9\.]+)(Ki|Mi|Gi|Ti|Ei|Zi){0,1}B$")
m = size_human_regex.match(size_human)
if not m:
if re.match("^\d+$", size_human):
# Assume size is already in bytes
return size_human
_print("WARN: '%s' is an invalid human size format" % size_human)
return None
number = None
fraction = 0
# check if number is fractional
f = re.match("(\d+)\.(\d+)", m.group(1))
if f:
number = int(f.group(1))
fraction = int(f.group(2))
else:
number = int(m.group(1))
unit = m.group(2)
if not unit:
unit = 'B'
for valid_unit in ['B', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']:
if unit == valid_unit:
if unit == 'B':
# cut any fraction if was given, as it is not valid
return str(number)
return int(number + fraction)
number *= 1024
fraction *= 1024
fraction /= 10
return int(number + fraction)
def size_bytes_2_size_human(num):
if not num:
return None
#Even if we receive string we convert so we can process it
num = int(num)
for unit in ['B','KiB','MiB','GiB','TiB','PiB','EiB','ZiB']:
if abs(num) < 1024.0:
size_human = "%3.1f%s" % (num, unit)
#round it down removing decimal numbers
size_human = re.sub("\.\d+", "", size_human)
return size_human
num /= 1024.0
#Very big number!!
size_human = "%.1f%s" % (num, 'Yi')
#round it down removing decimal numbers
size_human = re.sub("\.\d+", "", size_human)
return size_human
def install_package(pack):
"""
Install a package "pack" via `yum install -y `
"""
#Check if package is already installed
ret, ver = run("rpm -q %s" % pack, verbose=False, return_output=True)
if ret == 0:
_print("INFO: %s is already installed (%s)" % (pack, ver))
return True
if run("yum install -y %s" % pack) != 0:
msg = "FAIL: Could not install %s" % pack
_print(msg)
return False
_print("INFO: %s was successfully installed" % pack)
return True
def create_filesystem(vg_name, lv_name, filesystem="xfs"):
if filesystem not in ["xfs", "ext4", "btrfs"]:
_print("WARN: Unknown filesystem.")
return False
if run("mkfs.%s /dev/%s/%s" % (filesystem, vg_name, lv_name), verbose=True) != 0:
_print("WARN: Could not create filesystem %s on %s/%s" % (filesystem, vg_name, lv_name))
return False
return True
def metadata_snapshot(vg_name, lv_name):
if run("dmsetup suspend /dev/mapper/%s-%s-tpool" % (vg_name, lv_name), verbose=True) != 0:
_print("WARN: Device mapper could not suspend /dev/mapper/%s-%s-tpool" % (vg_name, lv_name))
return False
if run("dmsetup message /dev/mapper/%s-%s-tpool 0 reserve_metadata_snap" % (vg_name, lv_name), verbose=True) != 0:
_print("WARN: Device mapper could not create metadata snaphot on /dev/mapper/%s-%s-tpool" % (vg_name, lv_name))
return False
if run("dmsetup resume /dev/mapper/%s-%s-tpool" % (vg_name, lv_name), verbose=True) != 0:
_print("WARN: Device mapper could not resume /dev/mapper/%s-%s-tpool" % (vg_name, lv_name))
return False
return True
class LogChecker:
def __init__(self):
segfault_msg = " segfault "
calltrace_msg = "Call Trace:"
self.error_mgs = [segfault_msg, calltrace_msg]
def check_all(self):
"""Check for error on the system
Returns:
\tBoolean:
\t\tTrue is no error was found
\t\tFalse if some error was found
"""
_print("INFO: Checking for error on the system")
error = 0
if not self.kernel_check():
error += 1
if not self.abrt_check():
error += 1
if not self.messages_dump_check():
error += 1
if not self.dmesg_check():
error += 1
if not self.console_log_check():
error += 1
if not self.kdump_check():
error += 1
if error:
log_messages = "/var/log/messages"
if os.path.isfile(log_messages):
print("submit %s, named messages.log" % log_messages)
run("cp %s messages.log" % log_messages)
run("rhts-submit-log -l messages.log")
_print("INFO: Umounting NFS to avoid sosreport being hang there")
run("umount /var/crash")
ret_code = run("which sosreport", verbose=False)
if ret_code != 0:
_print("WARN: sosreport is not installed")
_print("INFO: Mounting NFS again")
run("mount /var/crash")
return False
print("Generating sosreport log")
disable_plugin = ""
if run("sosreport --list-plugins | grep emc") == 0:
disable_plugin = "-n emc"
ret_code, sosreport_log = run("sosreport --batch %s" % disable_plugin, return_output=True)
if ret_code != 0:
_print("WARN: sosreport command failed")
_print("INFO: Mounting NFS again")
run("mount /var/crash")
return False
sos_lines = sosreport_log.split("\n")
sos_file = None
for line in sos_lines:
#In RHEL7 sosreport is saving under /var/tmp while RHEL6 uses /tmp...
m = re.match(r"\s+((\/var)?\/tmp\/sosreport\S+)", line)
if m:
sos_file = m.group(1)
if not sos_file:
_print("WARN: could not save sosreport log")
_print("INFO: Mounting NFS again")
run("mount /var/crash")
return False
run("rhts-submit-log -l %s" % sos_file)
_print("INFO: Mounting NFS again")
run("mount /var/crash")
return False
return True
@staticmethod
def abrt_check():
"""Check if abrtd found any issue
Returns:
\tBoolean:
\t\tTrue no error was found
\t\tFalse some error was found
"""
_print("INFO: Checking abrt for error")
if run("rpm -q abrt", verbose=False) != 0:
_print("WARN: abrt tool does not seem to be installed")
_print("WARN: skipping abrt check")
return True
if run("pidof abrtd", verbose=False) != 0:
_print("WARN: abrtd is not running")
return False
ret, log = run("abrt-cli list", return_output=True)
if ret != 0:
_print("WARN: abrt-cli command failed")
return False
# We try to match for "Directory" to check if
# abrt-cli list is actually listing any issue
error = False
if log:
lines = log.split("\n")
for line in lines:
m = re.match(r"Directory:\s+(\S+)", line)
if m:
directory = m.group(1)
filename = directory
filename = filename.replace(":", "-")
filename += ".tar.gz"
run("tar cfzP %s %s" % (filename, directory))
run("rhts-submit-log -l %s" % filename)
# if log is saved on beaker, it can be deleted from server
# it avoids next test from detecting this failure
run("abrt-cli rm %s" % directory)
error = True
if error:
_print("WARN: Found abrt error")
return False
_print("PASS: no Abrt entry has been found.")
return True
@staticmethod
def kernel_check():
"""
Check if kernel got tainted.
It checks /proc/sys/kernel/tainted which returns a bitmask.
The values are defined in the kernel source file include/linux/kernel.h,
and explained in kernel/panic.c
cd /usr/src/kernels/`uname -r`/
Sources are provided by kernel-devel
Returns:
\tBoolean:
\t\tTrue if did not find any issue
\t\tFalse if found some issue
"""
_print("INFO: Checking for tainted kernel")
previous_tainted_file = "/tmp/previous-tainted"
ret, tainted = run("cat /proc/sys/kernel/tainted", return_output=True)
tainted_val = int(tainted)
if tainted_val == 0:
run("echo %d > %s" % (tainted_val, previous_tainted_file), verbose=False)
_print("PASS: Kernel is not tainted.")
return True
_print("WARN: Kernel is tainted!")
if not os.path.isfile(previous_tainted_file):
run("echo 0 > %s" % previous_tainted_file, verbose=False)
ret, prev_taint = run("cat %s" % previous_tainted_file, return_output=True)
prev_taint_val = int(prev_taint)
if prev_taint_val == tainted_val:
_print("INFO: Kernel tainted has already been handled")
return True
run("echo %d > %s" % (tainted_val, previous_tainted_file), verbose=False)
# check all bits that are set
bit = 0
while tainted_val != 0:
if tainted_val & 1:
_print("\tTAINT bit %d is set\n" % bit)
bit += 1
# shift tainted value
tainted_val /= 2
# List all tainted bits that are defined
print("List bit definition for tainted kernel")
run("cat /usr/src/kernels/`uname -r`/include/linux/kernel.h | grep TAINT_")
found_issue = False
# try to find the module which tainted the kernel, tainted module have a mark between '('')'
ret, output = run("cat /proc/modules | grep -e '(.*)' | cut -d' ' -f1", return_output=True)
tainted_mods = output.split("\n")
# For example during iscsi async_events scst tool loads an unsigned module
# just ignores it, so we will ignore this tainted if there is no tainted
# modules loaded
if not tainted_mods:
_print("INFO: ignoring tainted as the module is not loaded anymore")
else:
ignore_modules = ["ocrdma", "nvme_fc", "nvmet_fc"]
for tainted_mod in tainted_mods:
if tainted_mod:
_print("INFO: The following module got tainted: %s" % tainted_mod)
run("modinfo %s" % tainted_mod)
# we are ignoring ocrdma module
if tainted_mod in ignore_modules:
_print("INFO: ignoring tainted on %s" % tainted_mod)
run("echo %d > %s" % (tainted_val, previous_tainted_file), verbose=False)
continue
found_issue = True
run("echo %s > %s" % (tainted, previous_tainted_file), verbose=False)
if found_issue:
return False
return True
@staticmethod
def _date2num(date):
date_map = {"Jan": "1",
"Feb": "2",
"Mar": "3",
"Apr": "4",
"May": "5",
"Jun": "6",
"Jul": "7",
"Aug": "8",
"Sep": "9",
"Oct": "10",
"Nov": "11",
"Dec": "12"}
date_regex = r"(\S\S\S)\s(\d+)\s(\d\d:\d\d:\d\d)"
m = re.match(date_regex, date)
month = date_map[m.group(1)]
day = str(m.group(2))
# if day is a single digit, add '0' to begin
if len(day) == 1:
day = "0" + day
hour = m.group(3)
hour = hour.replace(":", "")
value = month + day + hour
return value
@staticmethod
def clear_dmesg():
cmd = "dmesg --clear"
if dist_ver() < 7:
cmd = "dmesg -c"
run(cmd, verbose=False)
return True
def messages_dump_check(self):
previous_time_file = "/tmp/previous-dump-check"
log_msg_file = "/var/log/messages"
if not os.path.isfile(log_msg_file):
_print("WARN: Could not open %s" % log_msg_file)
return True
log_file = open(log_msg_file)
log = log_file.read()
begin_tag = "\\[ cut here \\]"
end_tag = "\\[ end trace "
if not os.path.isfile(previous_time_file):
first_time = "Jan 01 00:00:00"
time = self._date2num(first_time)
run("echo %s > %s" % (time, previous_time_file))
# Read the last time test ran
ret, last_run = run("cat %s" % previous_time_file, return_output=True)
_print("INFO: Checking for stack dump messages after: %s" % last_run)
# Going to search the file for text that matches begin_tag until end_tag
dump_regex = begin_tag + "(.*?)" + end_tag
m = re.findall(dump_regex, log, re.MULTILINE)
if m:
_print("INFO: Checking if it is newer than: %s" % last_run)
print(m.group(1))
# TODO
_print("PASS: No recent dump messages has been found.")
return True
def dmesg_check(self):
"""
Check for error messages on dmesg ("Call Trace and segfault")
"""
_print("INFO: Checking for errors on dmesg.")
error = 0
for msg in self.error_mgs:
ret, output = run("dmesg | grep -i '%s'" % msg, return_output=True)
if output:
_print("WARN: found %s on dmesg" % msg)
run("echo '\nINFO found %s Saving it\n'>> dmesg.log" % msg)
run("dmesg >> dmesg.log")
run("rhts-submit-log -l dmesg.log")
error = + 1
self.clear_dmesg()
if error:
return False
_print("PASS: No errors on dmesg have been found.")
return True
def console_log_check(self):
"""
Checks for error messages on console log ("Call Trace and segfault")
"""
error = 0
console_log_file = "/root/console.log"
prev_console_log_file = "/root/console.log.prev"
new_console_log_file = "/root/console.log.new"
if not os.environ.get('LAB_CONTROLLER'):
_print("WARN: Could not find lab controller")
return True
if not os.environ.get('RECIPEID'):
_print("WARN: Could not find recipe ID")
return True
lab_controller = os.environ['LAB_CONTROLLER']
recipe_id = os.environ['RECIPEID']
# get current console log
url = "http://%s:8000/recipes/%s/logs/console.log" % (lab_controller, recipe_id)
if (run("wget -q %s -O %s" % (url, new_console_log_file)) != 0):
_print("INFO: Could not get console log")
# return sucess if could not get console.log
return True
# if there was previous console log, we just check the new part
run("diff -N -n --unidirectional-new-file %s %s > %s" % (
prev_console_log_file, new_console_log_file, console_log_file))
# backup the current full console.log
# next time we run the test we will compare just
# what has been appended to console.log
run("mv -f %s %s" % (new_console_log_file, prev_console_log_file))
_print("INFO: Checking for errors on %s" % console_log_file)
for msg in self.error_mgs:
ret, output = run("cat %s | grep -i '%s'" % (console_log_file, msg), return_output=True)
if output:
_print("INFO found %s on %s" % (msg, console_log_file))
run("rhts-submit-log -l %s" % console_log_file)
error = + 1
if error:
return False
_print("PASS: No errors on %s have been found." % console_log_file)
return True
@staticmethod
def kdump_check():
"""
Check for kdump error messages.
It assumes kdump is configured on /var/crash
"""
error = 0
previous_kdump_check_file = "/tmp/previous-kdump-check"
kdump_dir = "/var/crash"
ret, hostname = run("hostname", verbose=False, return_output=True)
if not os.path.exists("%s/%s" % (kdump_dir, hostname)):
_print("INFO: No kdump log found for this server")
return True
ret, output = run("ls -l %s/%s | awk '{print$9}'" % (kdump_dir, hostname), return_output=True)
kdumps = output.split("\n")
kdump_dates = []
for kdump in kdumps:
if kdump == "":
continue
# parse on the date, remove the ip of the uploader
m = re.match(".*?-(.*)", kdump)
if not m:
_print("WARN: unexpected format for kdump (%s)" % kdump)
continue
date = m.group(1)
# Old dump were using "."
date = date.replace(r"\.", "-")
# replace last "-" with space to format date properly
index = date.rfind("-")
date = date[:index] + " " + date[index + 1:]
_print("INFO: Found kdump from %s" % date)
kdump_dates.append(date)
# checks if a file to store last run exists, if not create it
if not os.path.isfile("%s" % previous_kdump_check_file):
# time in seconds
ret, time = run(r"date +\"\%s\"", verbose=False, return_output=True)
run("echo -n %s > %s" % (time, previous_kdump_check_file), verbose=False)
_print("INFO: kdump check is executing for the first time.")
_print("INFO: doesn't know from which date should check files.")
_print("PASS: Returning success.")
return True
# Read the last time test ran
ret, previous_check_time = run("cat %s" % previous_kdump_check_file, return_output=True)
# just add new line to terminal because the file should not have already new line character
print("")
for date in kdump_dates:
# Note %% is escape form to use % in a string
ret, kdump_time = run("date --date=\"%s\" +%%s" % date, return_output=True)
if ret != 0:
_print("WARN: Could not convert date %s" % date)
continue
if not kdump_time:
continue
if (int(kdump_time) > int(previous_check_time)):
_print("WARN: Found a kdump log from %s (more recent than %s)" % (date, previous_check_time))
_print("WARN: Check %s/%s" % (kdump_dir, hostname))
error += 1
ret, time = run(r"date +\"\%s\"", verbose=False, return_output=True)
run("echo -n %s > %s" % (time, previous_kdump_check_file), verbose=False)
if error:
return False
_print("PASS: No errors on kdump have been found.")
return True
class TestClass:
#we currently support these exit code for a test case
tc_sup_status = {"pass" : "PASS: ",
"fail" : "ERROR: ",
"skip" : "SKIP: "}
tc_pass = []
tc_fail = []
tc_skip = [] #For some reason it did not execute
tc_results = [] #Test results stored in a list
test_dir = "%s/.stqe-test" % expanduser("~")
test_log = "%s/test.log" % test_dir
def __init__(self):
print("################################## Test Init ###################################")
self.log_checker = LogChecker()
if not os.path.isdir(self.test_dir):
mkdir(self.test_dir)
# read entries on test.log, there will be entries if tend was not called
# before starting a TC class again, usually if the test case reboots the server
if not os.path.isfile(self.test_log):
#running the test for the first time
show_sys_info()
#Track memory usage during test
run("free -b > init_mem.txt", verbose=False)
run("top -b -n 1 > init_top.txt", verbose=False)
else:
try:
f = open(self.test_log)
file_data = f.read()
f.close()
except:
_print("WARN: TestClass() could not read %s" % self.test_log)
return
finally:
f.close()
log_entries = file_data.split("\n")
#remove the file, once tlog is ran it will add the entries again...
run("rm -f %s" % (self.test_log), verbose=False)
if log_entries:
_print("INFO: Loading test result from previous test run...")
for entry in log_entries:
self.tlog(entry)
print("################################################################################")
return
def tlog(self, string):
"""print message, if message begins with supported message status
the test message will be added to specific test result array
"""
print(string)
if re.match(self.tc_sup_status["pass"], string):
self.tc_pass.append(string)
self.tc_results.append(string)
run("echo '%s' >> %s" % (string, self.test_log), verbose=False)
if re.match(self.tc_sup_status["fail"], string):
self.tc_fail.append(string)
self.tc_results.append(string)
run("echo '%s' >> %s" % (string, self.test_log), verbose=False)
if re.match(self.tc_sup_status["skip"], string):
self.tc_skip.append(string)
self.tc_results.append(string)
run("echo '%s' >> %s" % (string, self.test_log), verbose=False)
return None
@staticmethod
def trun(cmd, return_output=False):
"""Run the cmd and format the log. return the exitint status of cmd
The arguments are:
\tCommand to run
\treturn_output: if should return command output as well (Boolean)
Returns:
\tint: Command exit code
\tstr: command output (optional)
"""
return run(cmd, return_output)
def tok(self, cmd, return_output=False):
"""Run the cmd and expect it to pass.
The arguments are:
\tCommand to run
\treturn_output: if should return command output as well (Boolean)
Returns:
\tBoolean: return_code
\t\tTrue: If command excuted successfully
\t\tFalse: Something went wrong
\tstr: command output (optional)
"""
cmd_code = None
ouput = None
ret_code = None
if not return_output:
cmd_code = run(cmd)
else:
cmd_code, output = run(cmd, return_output)
if cmd_code == 0:
self.tpass(cmd)
ret_code = True
else:
self.tfail(cmd)
ret_code = False
if return_output:
return ret_code, output
else:
return ret_code
def tnok(self, cmd, return_output=False):
"""Run the cmd and expect it to fail.
The arguments are:
\tCommand to run
\treturn_output: if should return command output as well (Boolean)
Returns:
\tBoolean: return_code
\t\tFalse: If command excuted successfully
\t\tTrue: Something went wrong
\tstr: command output (optional)
"""
cmd_code = None
ouput = None
ret_code = None
if not return_output:
cmd_code = run(cmd)
else:
cmd_code, output = run(cmd, return_output)
if cmd_code != 0:
self.tpass(cmd + " [exited with error, as expected]")
ret_code = True
else:
self.tfail(cmd + " [expected to fail, but it did not]")
ret_code = False
if return_output:
return ret_code, output
else:
return ret_code
def tpass(self, string):
"""Will add PASS + string to test log summary
"""
self.tlog(self.tc_sup_status["pass"] + string)
return None
def tfail(self, string):
"""Will add ERROR + string to test log summary
"""
self.tlog(self.tc_sup_status["fail"] + string)
return None
def tskip(self, string):
"""Will add SKIP + string to test log summary
"""
self.tlog(self.tc_sup_status["skip"] + string)
return None
def tend(self):
"""It checks for error in the system and print test summary
Returns:
\tBoolean
\t\tTrue if all test passed and no error was found on server
\t\tFalse if some test failed or found error on server
"""
if self.log_checker.check_all():
self.tpass("Search for error on the server")
else:
self.tfail("Search for error on the server")
print("################################ Test Summary ##################################")
#Will print test results in order and not by test result order
for tc in self.tc_results:
print(tc)
n_tc_pass = len(self.tc_pass)
n_tc_fail = len(self.tc_fail)
n_tc_skip = len(self.tc_skip)
print("#############################")
print("Total tests that passed: " + str(n_tc_pass))
print("Total tests that failed: " + str(n_tc_fail))
print("Total tests that skipped: " + str(n_tc_skip))
print("################################################################################")
sys.stdout.flush()
#Added this sleep otherwise some of the prints were not being shown....
sleep(1)
run("rm -f %s" % (self.test_log), verbose=False)
run("rmdir %s" % (self.test_dir), verbose=False)
#If at least one test failed, return error
if n_tc_fail > 0:
return False
return True
class LoopDev:
def __init__(self):
self.image_path = "/tmp"
@staticmethod
def _get_loop_path(name):
loop_path = name
if "/dev/" not in name:
loop_path = "/dev/" + name
return loop_path
@staticmethod
def _get_image_file(name, image_path):
image_file = "%s/%s.img" % (image_path, name)
return image_file
@staticmethod
def _standardize_name(name):
"""
Make sure use same standard for name, for example remove /dev/ from it if exists
"""
if not name:
_print("WARN: _standardize_name() - requires name as parameter")
return None
return name.replace("/dev/", "")
def create_loopdev(self, name=None, size=1024):
"""
Create a loop device
Parameters:
\tname: eg. loop0 (optional)
\tsize: Size in MB (default: 1024MB)
"""
ret_fail = False
if not name:
cmd = "losetup -f"
retcode, output = run(cmd, return_output=True, verbose=False)
if retcode != 0:
_print("WARN: Could not find free loop device")
print(output)
return None
name = output
ret_fail = None
name = self._standardize_name(name)
fname = self._get_image_file(name, self.image_path)
_print("INFO: Creating loop device %s with size %d" % (fname, size))
_print("INFO: Checking if %s exists" % fname)
if not os.path.isfile(fname):
# make sure we have enough space to create the file
free_space_bytes = get_free_space(self.image_path)
# Convert the size given in megabytes to bytes
size_bytes = int(size_human_2_size_bytes("%sMiB" % size))
if free_space_bytes <= size_bytes:
_print("WARN: Not enough space to create loop device with size %s"
% size_bytes_2_size_human(size_bytes))
_print("available space: %s" % size_bytes_2_size_human(free_space_bytes))
return ret_fail
_print("INFO: Creating file %s" % fname)
# cmd = "dd if=/dev/zero of=%s seek=%d bs=1M count=0" % (fname, size)
cmd = "fallocate -l %sM %s" % (size, fname)
try:
# We are just creating the file, not writting zeros to it
retcode = run(cmd)
if retcode != 0:
_print("command failed with code %s" % retcode)
_print("WARN: Could not create loop device image file")
return ret_fail
except OSError as e:
print("command failed: ", e, file=sys.err)
return ret_fail
loop_path = self._get_loop_path(name)
# detach loop device if it exists
self.detach_loopdev(loop_path)
# Going to associate the file to the loopdevice
cmd = "losetup %s %s" % (loop_path, fname)
retcode = run(cmd)
if retcode != 0:
_print("WARN: Could not create loop device")
return ret_fail
if ret_fail is None:
return loop_path
return True
def delete_loopdev(self, name):
"""
Delete a loop device
Parameters:
\tname: eg. loop0 or /dev/loop0
"""
if not name:
_print("WARN: delete_loopdev() - requires name parameter")
return False
_print("INFO: Deleting loop device %s" % name)
name = self._standardize_name(name)
loop_path = self._get_loop_path(name)
# detach loop device if it exists
if not self.detach_loopdev(name):
_print("WARN: could not detach %s" % loop_path)
return False
fname = self._get_image_file(name, self.image_path)
if os.path.isfile(fname):
cmd = "rm -f %s" % fname
retcode = run(cmd)
if retcode != 0:
_print("WARN: Could not delete loop device %s" % name)
return False
# check if loopdev file is deleted as it sometimes remains
if os.path.isfile(fname):
_print("WARN: Deleted loop device file %s but it is still there" % fname)
return False
return True
@staticmethod
def get_loopdev():
# example of output on rhel-6.7
# /dev/loop0: [fd00]:396428 (/tmp/loop0.img)
retcode, output = run("losetup -a | awk '{print$1}'", return_output=True, verbose=False)
# retcode, output = run("losetup -l | tail -n +2", return_output=True, verbose=False)
if (retcode != 0):
_print("WARN: get_loopdev failed to execute")
print(output)
return None
devs = None
if output:
devs = output.split("\n")
# remove the ":" character from all devices
devs = [d.replace(':', "") for d in devs]
return devs
def detach_loopdev(self, name=None):
cmd = "losetup -D"
if name:
devs = self.get_loopdev()
if not devs:
# No device was found
return False
name = self._standardize_name(name)
# Just try to detach if device is connected, otherwise ignore
# print("INFO: Checking if ", loop_path, " exists, to be detached")
dev_path = self._get_loop_path(name)
if dev_path in devs:
cmd = "losetup -d %s" % dev_path
else:
# if loop device does not exist just ignore it
return True
# run losetup -D or -d <device>
retcode = run(cmd)
if retcode != 0:
_print("WARN: Could not detach loop device")
return False
return True
class LVM:
###########################################
# VG section
###########################################
@staticmethod
def vg_query(verbose=False):
"""Query Volume Groups and return a dictonary with VG information for each VG.
The arguments are:
\tNone
Returns:
\tdict: Return a dictionary with VG info for each VG
"""
cmd = "vgs --noheadings --separator \",\""
retcode, output = run(cmd, return_output=True, verbose=verbose)
if (retcode != 0):
_print("INFO: there is no VGs")
return None
vgs = output.split("\n")
# format of VG info: name #PV #LV #SN Attr VSize VFree
vg_info_regex = "\s+(\S+),(\S+),(\S+),(.*),(.*),(.*),(.*)$"
vg_dict = {}
for vg in vgs:
m = re.match(vg_info_regex, vg)
if not m:
continue
vg_info_dict = {"num_pvs": m.group(2),
"num_lvs": m.group(3),
"num_sn": m.group(4), # not sure what it is
"attr": m.group(5),
"vsize": m.group(6),
"vfree": m.group(7)}
vg_dict[m.group(1)] = vg_info_dict
return vg_dict
@staticmethod
def vg_create(vg_name, pv_name, force=False, verbose=True):
"""Create a Volume Group.
The arguments are:
\tPV name
Returns:
\tBoolean:
\t\tTrue if success
\t'tFalse in case of failure
"""
if not vg_name or not pv_name:
_print("WARN: vg_create requires vg_name and pv_name")
return False
options = ""
if force:
options += "--force"
cmd = "vgcreate %s %s %s" % (options, vg_name, pv_name)
retcode = run(cmd, verbose=verbose)
if (retcode != 0):
# _print("WARN: Could not create %s" % vg_name)
return False
return True
def vg_remove(self, vg_name, force=False, verbose=True):
"""Delete a Volume Group.
The arguments are:
\tVG name
\tforce (boolean)
Returns:
\tBoolean:
\t\tTrue if success
\t'tFalse in case of failure
"""
if not vg_name:
_print("WARN: vg_remove requires vg_name")
return False
vg_dict = self.vg_query()
if vg_name not in vg_dict.keys():
_print("INFO: vg_remove - %s does not exist. Skipping..." % vg_name)
return True
options = ""
if force:
options += "--force"
cmd = "vgremove %s %s" % (options, vg_name)
retcode = run(cmd, verbose=verbose)
if (retcode != 0):
return False
return True
###########################################
# LV section
###########################################
@staticmethod
def lv_query(options=None, verbose=False):
"""Query Logical Volumes and return a dictonary with LV information for each LV.
The arguments are:
\toptions: If not want to use default lvs output. Use -o for no default fields
Returns:
\tdict: Return a list with LV info for each LV
"""
# Use \",\" as separator, as some output might contain ','
# For example, lvs -o modules on thin device returns "thin,thin-pool"
cmd = "lvs -a --noheadings --separator \\\",\\\""
# format of LV info: Name VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert
lv_info_regex = "\s+(\S+)\",\"(\S+)\",\"(\S+)\",\"(\S+)\",\"(.*)\",\"(.*)\",\"(.*)\",\"(.*)\",\"(.*)\",\"(.*)\",\"(.*)\",\"(.*)$"
# default parameters returned by lvs -a
param_names = ["name", "vg_name", "attr", "size", "pool", "origin", "data_per", "meta_per", "move", "log",
"copy_per", "convert"]
if options:
param_names = ["name", "vg_name"]
# need to change default regex
lv_info_regex = "\s+(\S+)\",\"(\S+)"
parameters = options.split(",")
for param in parameters:
lv_info_regex += "\",\"(.*)"
param_names.append(param)
lv_info_regex += "$"
cmd += " -o lv_name,vg_name,%s" % options
retcode, output = run(cmd, return_output=True, verbose=verbose)
if (retcode != 0):
_print("INFO: there is no LVs")
return None
lvs = output.split("\n")
lv_list = []
for lv in lvs:
m = re.match(lv_info_regex, lv)
if not m:
_print("WARN: (%s) does not match lvs output format" % lv)
continue
lv_info_dict = {}
for index in range(len(param_names)):
lv_info_dict[param_names[index]] = m.group(index + 1)
lv_list.append(lv_info_dict)
return lv_list
def lv_info(self, lv_name, vg_name, options=None, verbose=False):
"""
Show information of specific LV
"""
if not lv_name or not vg_name:
_print("WARN: lv_info() - requires lv_name and vg_name as parameters")
return None
lvs = self.lv_query(options=options, verbose=verbose)
if not lvs:
return None
for lv in lvs:
if (lv["name"] == lv_name and
lv["vg_name"] == vg_name):
return lv
return None
@staticmethod
def lv_create(vg_name, lv_name, options=(""), verbose=True):
"""Create a Logical Volume.
The arguments are:
\tVG name
\tLV name
\toptions
Returns:
\tBoolean:
\t\tTrue if success
\t'tFalse in case of failure
"""
if not vg_name or not lv_name:
_print("WARN: lv_create requires vg_name and lv_name")
return False
cmd = "lvcreate %s %s -n %s" % (" ".join(str(i) for i in options), vg_name, lv_name)
retcode = run(cmd, verbose=verbose)
if (retcode != 0):
# _print("WARN: Could not create %s" % lv_name)
return False
return True
@staticmethod
def lv_activate(lv_name, vg_name, verbose=True):
"""Activate a Logical Volume
The arguments are:
\tLV name
\tVG name
Returns:
\tBoolean:
\t\tTrue in case of success
\t\tFalse if something went wrong
"""
if not lv_name or not vg_name:
_print("WARN: lv_activate requires lv_name and vg_name")
return False
cmd = "lvchange -ay %s/%s" % (vg_name, lv_name)
retcode = run(cmd, verbose=verbose)
if (retcode != 0):
_print("WARN: Could not activate LV %s" % lv_name)
return False
# Maybe we should query the LVs and make sure it is really activated
return True
@staticmethod
def lv_deactivate(lv_name, vg_name, verbose=True):
"""Deactivate a Logical Volume
The arguments are:
\tLV name
\tVG name
Returns:
\tBoolean:
\t\tTrue in case of success
\t\tFalse if something went wrong
"""
if not lv_name or not vg_name:
_print("WARN: lv_deactivate requires lv_name and vg_name")
return False
cmd = "lvchange -an %s/%s" % (vg_name, lv_name)
retcode = run(cmd, verbose=verbose)
if (retcode != 0):
_print("WARN: Could not deactivate LV %s" % lv_name)
return False
# Maybe we should query the LVs and make sure it is really deactivated
return True
def lv_remove(self, lv_name, vg_name, verbose=True):
"""Remove an LV from a VG
The arguments are:
\tLV name
\tVG name
Returns:
\tBoolean:
\t\tTrue in case of success
\t\tFalse if something went wrong
"""
if not lv_name or not vg_name:
_print("WARN: lv_remove requires lv_name and vg_name")
return False
lvs = self.lv_query()
lv_names = lv_name.split()
for lv_name in lv_names:
if not self.lv_info(lv_name, vg_name):
_print("INFO: lv_remove - LV %s does not exist. Skipping" % lv_name)
continue
cmd = "lvremove --force %s/%s" % (vg_name, lv_name)
retcode = run(cmd, verbose=verbose)
if (retcode != 0):
_print("WARN: Could not remove LV %s" % lv_name)
return False
if self.lv_info(lv_name, vg_name):
_print("INFO: lv_remove - LV %s still exists." % lv_name)
return False
return True
@staticmethod
def lv_convert(vg_name, lv_name, options, verbose=True):
"""Change Logical Volume layout.
The arguments are:
\tVG name
\tLV name
\toptions
Returns:
\tBoolean:
\t\tTrue if success
\t'tFalse in case of failure
"""
if not options:
_print("WARN: lv_convert requires at least some options specified.")
return False
if not lv_name or not vg_name:
_print("WARN: lv_convert requires vg_name and lv_name")
return False
cmd = "lvconvert %s %s/%s" % (" ".join(options), vg_name, lv_name)
retcode = run(cmd, verbose=verbose)
if retcode != 0:
_print("WARN: Could not convert %s" % lv_name)
return False
return True
###########################################
# Config file
###########################################
@staticmethod
def get_config_file_path():
return "/etc/lvm/lvm.conf"
def update_config(self, key, value):
config_file = self.get_config_file_path()
search_regex = re.compile("(\s*)%s(\s*)=(\s*)\S*" % key)
for line in fileinput.input(config_file, inplace=1):
m = search_regex.match(line)
if m:
line = "%s%s = %s" % (m.group(1), key, value)
# print saves the line to the file
# need to remove new line character as print will add it
line = line.rstrip('\n')
print(line)
class DMPD:
def __init__(self):
self.lvm = LVM()
def _get_devices(self):
lv_list = self.lvm.lv_query()
return lv_list
@staticmethod
def _get_active_devices():
cmd = "ls /dev/mapper/"
retcode, output = run(cmd, return_output=True, verbose=False)
if retcode != 0:
_print("WARN: Could not find active dm devices")
return False
devices = output.split()
return devices
@staticmethod
def _get_device_path(vg_name, lv_name):
device_path = vg_name + "-" + lv_name
if "/dev/mapper/" not in device_path:
device_path = "/dev/mapper/" + device_path
return device_path
def _check_device(self, vg_name, lv_name):
devices = self._get_devices()
device_list = [x["name"] for x in devices]
if lv_name not in device_list:
_print("WARN: %s is not a device" % lv_name)
return False
for x in devices:
if x["name"] == lv_name and x["vg_name"] == vg_name:
_print("INFO: Found device %s in group %s" % (lv_name, vg_name))
return True
return False
def _activate_device(self, vg_name, lv_name):
devices_active = self._get_active_devices()
if vg_name + "-" + lv_name not in devices_active:
ret = self.lvm.lv_activate(lv_name, vg_name)
if not ret:
_print("WARN: Could not activate device %s" % lv_name)
return False
_print("INFO: device %s was activated" % lv_name)
_print("INFO: device %s is active" % lv_name)
return True
@staticmethod
def _fallocate(_file, size, command_message):
cmd = "fallocate -l %sM %s" % (size, _file)
try:
retcode = run(cmd)
if retcode != 0:
_print("WARN: Command failed with code %s." % retcode)
_print("WARN: Could not create file to %s metadata to." % command_message)
return False
except OSError as e:
print("command failed: ", e, file=sys.err)
return False
return True
@staticmethod
def get_help(cmd):
commands = ["cache_check", "cache_dump", "cache_metadata_size", "cache_repair", "cache_restore", "era_check",
"era_dump", "era_invalidate", "era_restore", "thin_check", "thin_delta", "thin_dump", "thin_ls",
"thin_metadata_size", "thin_repair", "thin_restore", "thin_rmap", "thin_show_duplicates",
"thin_trim"]
if cmd not in commands:
_print("WARN: Unknown command %s" % cmd)
return False
command = "%s -h" % cmd
retcode = run(command, verbose=True)
if retcode != 0:
_print("WARN: Could not get help for %s." % cmd)
return False
return True
@staticmethod
def get_version(cmd):
commands = ["cache_check", "cache_dump", "cache_metadata_size", "cache_repair", "cache_restore", "era_check",
"era_dump", "era_invalidate", "era_restore", "thin_check", "thin_delta", "thin_dump", "thin_ls",
"thin_metadata_size", "thin_repair", "thin_restore", "thin_rmap", "thin_show_duplicates",
"thin_trim"]
if cmd not in commands:
_print("WARN: Unknown command %s" % cmd)
return False
command = "%s -V" % cmd
retcode = run(command, verbose=True)
if retcode != 0:
_print("WARN: Could not get version of %s." % cmd)
return False
return True
def _get_dev_id(self, dev_id, path=None, lv_name=None, vg_name=None):
dev_ids = []
if path is None:
retcode, data = self.thin_dump(source_vg=vg_name, source_lv=lv_name, formatting="xml", return_output=True)
if not retcode:
_print("WARN: Could not dump metadata from %s/%s" % (vg_name, lv_name))
return False
data_lines = data.splitlines()
for line in data_lines:
blocks = line.split()
for block in blocks:
if not block.startswith("dev_"):
continue
else:
dev_ids.append(int(block[8:-1]))
else:
with open(path, "r") as meta:
for line in meta:
blocks = line.split()
for block in blocks:
if not block.startswith("dev_"):
continue
else:
dev_ids.append(int(block[8:-1]))
if dev_id in dev_ids:
return True
return False
@staticmethod
def _metadata_size(source=None, lv_name=None, vg_name=None):
if source is None:
cmd = "lvs -a --units m"
ret, data = run(cmd, return_output=True)
if ret != 0:
_print("WARN: Could not list LVs")
data_line = data.splitlines()
for line in data_line:
cut = line.split()
if not cut or lv_name != cut[0] and vg_name != cut[1]:
continue
cut = cut[3]
cut = cut.split("m")
size = float(cut[0])
cmd = "rm -f /tmp/meta_size"
run(cmd)
return int(size)
_print("WARN: Could not find %s %s in lvs, setting size to 100m" % (lv_name, vg_name))
return 100
else:
return int(os.stat(source).st_size) / 1000000
###########################################
# cache section
###########################################
def cache_check(self, source_file=None, source_vg=None, source_lv=None, quiet=False, super_block_only=False,
clear_needs_check_flag=False, skip_mappings=False, skip_hints=False, skip_discards=False,
verbose=True):
"""Check cache pool metadata from either file or device.
The arguments are:
\tsource_file
\tsource_vg VG name
\tsource_lv LV name
\tquiet Mute STDOUT
\tsuper_block_only
\tclear_needs_check_flag
\tskip_mappings
\tskip_hints
\tskip_discards
Returns:
\tBoolean:
\t\tTrue if success
\t'tFalse in case of failure
"""
options = ""
if not source_file and (not source_vg or not source_lv):
_print("WARN: cache_check requires either source_file OR source_vg and source_lv.")
return False
if not source_file:
ret = self._check_device(source_vg, source_lv)
if not ret:
return False
ret = self._activate_device(source_vg, source_lv)
if not ret:
return False
device = self._get_device_path(source_vg, source_lv)
else:
if not os.path.isfile(source_file):
_print("WARN: Source file is not a file.")
return False
device = source_file
if quiet:
options += "--quiet "
if super_block_only:
options += "--super-block-only "
if clear_needs_check_flag:
options += "--clear-needs-check-flag "
if skip_mappings:
options += "--skip-mappings "
if skip_hints:
options += "--skip-hints "
if skip_discards:
options += "--skip-discards "
cmd = "cache_check %s %s" % (device, options)
retcode = run(cmd, verbose=verbose)
if retcode != 0:
_print("WARN: Could not check %s metadata" % device)
return False
return True
def cache_dump(self, source_file=None, source_vg=None, source_lv=None, output=None, repair=False, verbose=True,
return_output=False):
"""Dumps cache metadata from device of source file to standard output or file.
The arguments are:
\tsource_file
\tsource_vg VG name
\tsource_lv LV name
\toutput specify output xml file
\treturn_output see 'Returns', not usable with output=True
\trepair Repair the metadata while dumping it
Returns:
\tOnly Boolean if return_output False:
\t\tTrue if success
\t'tFalse in case of failure
\tBoolean and data if return_output True
"""
options = ""
if return_output and output:
_print("INFO: Cannot return to both STDOUT and file, returning only to file.")
return_output = False
if return_output:
ret_fail = (False, None)
else:
ret_fail = False
if not source_file and (not source_vg or not source_lv):
_print("WARN: cache_dump requires either source_file OR source_vg and source_lv.")
return ret_fail
if not source_file:
ret = self._check_device(source_vg, source_lv)
if not ret:
return ret_fail
ret = self._activate_device(source_vg, source_lv)
if not ret:
return ret_fail
device = self._get_device_path(source_vg, source_lv)
else:
if not os.path.isfile(source_file):
_print("WARN: Source file is not a file.")
return ret_fail
device = source_file
if output:
if not os.path.isfile(output):
size = self._metadata_size(source_file, source_lv, source_vg)
ret = self._fallocate(output, size + 1, "dump")
if not ret:
return ret_fail
options += "-o %s " % output
if repair:
options += "--repair"
cmd = "cache_dump %s %s" % (device, options)
if return_output:
retcode, data = run(cmd, return_output=True, verbose=verbose)
else:
retcode = run(cmd, verbose=verbose)
if retcode != 0:
_print("WARN: Could not dump %s metadata." % device)
return ret_fail
if return_output:
return True, data
return True
def cache_repair(self, source_file=None, source_vg=None, source_lv=None, target_file=None, target_vg=None,
target_lv=None, verbose=True):
"""Repairs cache metadata from source file/device to target file/device
The arguments are:
\tsource as either source_file OR source_vg and source_lv
\ttarget as either target_file OR target_vg and target_lv
Returns:
\tBoolean:
\t\tTrue if success
\t'tFalse in case of failure
"""
if not source_file and (not source_vg or not source_lv):
_print("WARN: cache_repair requires either source_file OR source_vg and source_lv as source.")
return False
if not target_file and (not target_vg or not target_lv):
_print("WARN: cache_repair requires either target_file OR target_vg and target_lv as target.")
return False
if not source_file:
ret = self._check_device(source_vg, source_lv)
if not ret:
return False
ret = self._activate_device(source_vg, source_lv)
if not ret:
return False
source = self._get_device_path(source_vg, source_lv)
else:
if not os.path.isfile(source_file):
_print("WARN: Source file is not a file.")
return False
source = source_file
if not target_file:
ret = self._check_device(target_vg, target_lv)
if not ret:
return False
ret = self._activate_device(target_vg, target_lv)
if not ret:
return False
target = self._get_device_path(target_vg, target_lv)
else:
if not os.path.isfile(target_file):
size = self._metadata_size(source_file, source_lv, source_vg)
ret = self._fallocate(target_file, size + 1, "repair")
if not ret:
return False
target = target_file
cmd = "cache_repair -i %s -o %s" % (source, target)
retcode = run(cmd, verbose=verbose)
if retcode != 0:
_print("WARN: Could not repair metadata from %s to %s" % (source, target))
return False
return True
def cache_restore(self, source_file, target_vg=None, target_lv=None, target_file=None, quiet=False,
metadata_version=None, omit_clean_shutdown=False, override_metadata_version=None, verbose=True):
"""Restores cache metadata from source xml file to target device/file
The arguments are:
\tsource_file Source xml file
\ttarget as either target_file OR target_vg and target_lv
\tquiet Mute STDOUT
\tmetadata_version Specify metadata version to restore
\tomit_clean_shutdown Disable clean shutdown
\toverride_metadata_version DEBUG option to override metadata version without checking
Returns:
\tBoolean:
\t\tTrue if success
\t'tFalse in case of failure
"""
options = ""
if source_file is None:
_print("WARN: cache_restore requires source file.")
return False
if not target_file and (not target_vg or not target_lv):
_print("WARN: cache_restore requires either target_file OR target_vg and target_lv as target.")
return False
if not os.path.isfile(source_file):
_print("WARN: Source file is not a file.")
return False
if not target_file:
ret = self._check_device(target_vg, target_lv)
if not ret:
return False
ret = self._activate_device(target_vg, target_lv)
if not ret:
return False
target = self._get_device_path(target_vg, target_lv)
else:
if not os.path.isfile(target_file):
size = self._metadata_size(source_file)
ret = self._fallocate(target_file, size + 1, "restore")
if not ret:
return False
target = target_file
if quiet:
options += "--quiet "
if metadata_version:
options += "--metadata-version %s " % metadata_version
if omit_clean_shutdown:
options += "--omit-clean-shutdown "
if override_metadata_version:
options += "--debug-override-metadata-version %s" % override_metadata_version
cmd = "cache_restore -i %s -o %s %s" % (source_file, target, options)
retcode = run(cmd, verbose=verbose)
if retcode != 0:
_print("WARN: Could not restore metadata from %s to %s" % (source_file, target))
return False
return True
###########################################
# thinp section
###########################################
def thin_check(self, source_file=None, source_vg=None, source_lv=None, quiet=False, super_block_only=False,
clear_needs_check_flag=False, skip_mappings=False, ignore_non_fatal_errors=False, verbose=True):
"""Check thin pool metadata from either file or device.
The arguments are:
\tsource_file
\tsource_vg VG name
\tsource_lv LV name
\tquiet Mute STDOUT
\tsuper_block_only
\tclear_needs_check_flag
\tskip_mappings
\tignore_non_fatal_errors
Returns:
\tBoolean:
\t\tTrue if success
\t'tFalse in case of failure
"""
options = ""
if not source_file and (not source_vg or not source_lv):
_print("WARN: thin_check requires either source_file OR source_vg and source_lv.")
return False
if not source_file:
ret = self._check_device(source_vg, source_lv)
if not ret:
return False
ret = self._activate_device(source_vg, source_lv)
if not ret:
return False
device = self._get_device_path(source_vg, source_lv)
else:
if not os.path.isfile(source_file):
_print("WARN: Source file is not a file.")
return False
device = source_file
if quiet:
options += "--quiet "
if super_block_only:
options += "--super-block-only "
if clear_needs_check_flag:
options += "--clear-needs-check-flag "
if skip_mappings:
options += "--skip-mappings "
if ignore_non_fatal_errors:
options += "--ignore-non-fatal-errors "
cmd = "thin_check %s %s" % (device, options)
retcode = run(cmd, verbose=verbose)
if retcode != 0:
_print("WARN: Could not check %s metadata" % device)
return False
return True
def thin_ls(self, source_vg, source_lv, no_headers=False, fields=None, snapshot=False, verbose=True):
"""List information about thin LVs on thin pool.
The arguments are:
\tsource_vg VG name
\tsource_lv LV name
\tfields list of fields to output, default is all
\tsnapshot If use metadata snapshot, able to run on live snapshotted pool
Returns:
\tBoolean:
\t\tTrue if success
\t'tFalse in case of failure
"""
options = ""
if not source_vg or not source_lv:
_print("WARN: thin_ls requires source_vg and source_lv.")
return False
ret = self._check_device(source_vg, source_lv)
if not ret:
return False
ret = self._activate_device(source_vg, source_lv)
if not ret:
return False
device = self._get_device_path(source_vg, source_lv)
if no_headers:
options += "--no-headers "
fields_possible = ["DEV", "MAPPED_BLOCKS", "EXCLUSIVE_BLOCKS", "SHARED_BLOCKS", "MAPPED_SECTORS",
"EXCLUSIVE_SECTORS", "SHARED_SECTORS", "MAPPED_BYTES", "EXCLUSIVE_BYTES", "SHARED_BYTES",
"MAPPED", "EXCLUSIVE", "TRANSACTION", "CREATE_TIME", "SHARED", "SNAP_TIME"]
if fields is None:
options += " --format \"%s\" " % ",".join([str(i) for i in fields_possible])
else:
for field in fields:
if field not in fields_possible:
_print("WARN: Unknown field %s specified." % field)
_print("INFO: Possible fields are: %s" % ", ".join([str(i) for i in fields_possible]))
return False
options += " --format \"%s\" " % ",".join([str(i) for i in fields])
if snapshot:
options += "--metadata-snap"
cmd = "thin_ls %s %s" % (device, options)
retcode = run(cmd, verbose=verbose)
if retcode != 0:
_print("WARN: Could not list %s metadata" % device)
return False
return True
def thin_dump(self, source_file=None, source_vg=None, source_lv=None, output=None, repair=False, formatting=None,
snapshot=None, dev_id=None, skip_mappings=False, verbose=True, return_output=False):
"""Dumps thin metadata from device of source file to standard output or file.
The arguments are:
\tsource_file
\tsource_vg VG name
\tsource_lv LV name
\toutput specify output xml file
\treturn_output see 'Returns', not usable with output=True
\trepair Repair the metadata while dumping it
\tformatting Specify output format [xml, human_readable, custom='file']
\tsnapshot (Boolean/Int) Use metadata snapshot. If Int provided, specifies block number
\tdev_id ID of the device
Returns:
\tOnly Boolean if return_output False:
\t\tTrue if success
\t'tFalse in case of failure
\tBoolean and data if return_output True
"""
options = ""
if return_output and output:
_print("INFO: Cannot return to both STDOUT and file, returning only to file.")
return_output = False
if return_output:
ret_fail = (False, None)
else:
ret_fail = False
if not source_file and (not source_vg or not source_lv):
_print("WARN: thin_dump requires either source_file OR source_vg and source_lv.")
return ret_fail
if not source_file:
ret = self._check_device(source_vg, source_lv)
if not ret:
return ret_fail
ret = self._activate_device(source_vg, source_lv)
if not ret:
return ret_fail
device = self._get_device_path(source_vg, source_lv)
else:
if not os.path.isfile(source_file):
_print("WARN: Source file is not a file.")
return ret_fail
device = source_file
if output:
if not os.path.isfile(output):
size = self._metadata_size(source_file, source_lv, source_vg)
ret = self._fallocate(output, size + 1, "dump")
if not ret:
return ret_fail
options += "-o %s " % output
if repair:
options += "--repair "
if snapshot:
if isinstance(snapshot, bool):
options += "--metadata-snap "
elif isinstance(snapshot, int):
options += "--metadata-snap %s " % snapshot
else:
_print("WARN: Unknown snapshot value, use either Boolean or Int.")
return ret_fail
if formatting:
if formatting in ["xml", "human_readable"]:
options += "--format %s " % formatting
elif formatting.startswith("custom="):
if not os.path.isfile(formatting[8:-1]):
_print("WARN: Specified custom formatting file is not a file.")
return ret_fail
options += "--format %s " % formatting
else:
_print("WARN: Unknown formatting specified, please use one of [xml, human_readable, custom='file'].")
return ret_fail
if dev_id:
if isinstance(dev_id, int):
if self._get_dev_id(dev_id, source_file, source_lv, source_vg):
options += "--dev-id %s " % dev_id
else:
_print("WARN: Unknown dev_id value, device with ID %s does not exist." % dev_id)
return ret_fail
else:
_print("WARN: Unknown dev_id value, must be Int.")
return ret_fail
if skip_mappings:
options += "--skip-mappings "
cmd = "thin_dump %s %s" % (device, options)
if return_output:
retcode, data = run(cmd, return_output=True, verbose=verbose)
else:
retcode = run(cmd, verbose=verbose)
if retcode != 0:
_print("WARN: Could not dump %s metadata." % device)
return ret_fail
if return_output:
return True, data
return True
def thin_restore(self, source_file, target_vg=None, target_lv=None, target_file=None, quiet=False, verbose=True):
"""Restores thin metadata from source xml file to target device/file
The arguments are:
\tsource_file Source xml file
\ttarget as either target_file OR target_vg and target_lv
\tquiet Mute STDOUT
\tmetadata_version Specify metadata version to restore
\tomit_clean_shutdown Disable clean shutdown
\toverride_metadata_version DEBUG option to override metadata version without checking
Returns:
\tBoolean:
\t\tTrue if success
\t'tFalse in case of failure
"""
options = ""
if source_file is None:
_print("WARN: thin_restore requires source file.")
return False
if not target_file and (not target_vg or not target_lv):
_print("WARN: thin_restore requires either target_file OR target_vg and target_lv as target.")
return False
if not os.path.isfile(source_file):
_print("WARN: Source file is not a file.")
return False
if not target_file:
ret = self._check_device(target_vg, target_lv)
if not ret:
return False
ret = self._activate_device(target_vg, target_lv)
if not ret:
return False
target = self._get_device_path(target_vg, target_lv)
else:
if not os.path.isfile(target_file):
size = self._metadata_size(source_file)
ret = self._fallocate(target_file, size + 1, "restore")
if not ret:
return False
target = target_file
if quiet:
options += "--quiet"
cmd = "thin_restore -i %s -o %s %s" % (source_file, target, options)
retcode = run(cmd, verbose=verbose)
if retcode != 0:
_print("WARN: Could not restore metadata from %s to %s" % (source_file, target))
return False
return True
def thin_repair(self, source_file=None, source_vg=None, source_lv=None, target_file=None, target_vg=None,
target_lv=None, verbose=True):
"""Repairs thin metadata from source file/device to target file/device
The arguments are:
\tsource as either source_file OR source_vg and source_lv
\ttarget as either target_file OR target_vg and target_lv
Returns:
\tBoolean:
\t\tTrue if success
\t'tFalse in case of failure
"""
if not source_file and (not source_vg or not source_lv):
_print("WARN: thin_repair requires either source_file OR source_vg and source_lv as source.")
return False
if not target_file and (not target_vg or not target_lv):
_print("WARN: thin_repair requires either target_file OR target_vg and target_lv as target.")
return False
if not source_file:
ret = self._check_device(source_vg, source_lv)
if not ret:
return False
ret = self._activate_device(source_vg, source_lv)
if not ret:
return False
source = self._get_device_path(source_vg, source_lv)
else:
if not os.path.isfile(source_file):
_print("WARN: Source file is not a file.")
return False
source = source_file
if not target_file:
ret = self._check_device(target_vg, target_lv)
if not ret:
return False
ret = self._activate_device(target_vg, target_lv)
if not ret:
return False
target = self._get_device_path(target_vg, target_lv)
else:
if not os.path.isfile(target_file):
size = self._metadata_size(source_file, source_lv, source_vg)
ret = self._fallocate(target_file, size + 1, "repair")
if not ret:
return False
target = target_file
cmd = "thin_repair -i %s -o %s" % (source, target)
retcode = run(cmd, verbose=verbose)
if retcode != 0:
_print("WARN: Could not repair metadata from %s to %s" % (source, target))
return False
return True
def thin_rmap(self, region, source_file=None, source_vg=None, source_lv=None, verbose=True):
"""Output reverse map of a thin provisioned region of blocks from metadata device.
The arguments are:
\tsource_vg VG name
\tsource_lv LV name
Returns:
\tBoolean:
\t\tTrue if success
\t'tFalse in case of failure
"""
if not source_file and (not source_vg or not source_lv):
_print("WARN: thin_rmap requires either source_file OR source_vg and source_lv as source.")
return False
if not source_file:
ret = self._check_device(source_vg, source_lv)
if not ret:
return False
ret = self._activate_device(source_vg, source_lv)
if not ret:
return False
device = self._get_device_path(source_vg, source_lv)
else:
if not os.path.isfile(source_file):
_print("WARN: Source file is not a file.")
return False
device = source_file
regions = region.split(".")
try:
int(regions[0])
if regions[1] != '':
raise ValueError
int(regions[2])
if regions[3] is not None:
raise ValueError
except ValueError:
_print("WARN: Region must be in format 'INT..INT'")
return False
except IndexError:
pass
# region 1..-1 must be valid, using usigned 32bit ints
if int(regions[0]) & 0xffffffff >= int(regions[2]) & 0xffffffff:
_print("WARN: Beginning of the region must be before its end.")
return False
options = "--region %s" % region
cmd = "thin_rmap %s %s" % (device, options)
retcode = run(cmd, verbose=verbose)
if retcode != 0:
_print("WARN: Could not output reverse map from %s metadata device" % device)
return False
return True
def thin_trim(self, target_vg, target_lv, force=True, verbose=True):
"""Issue discard requests for free pool space.
The arguments are:
\ttarget_vg VG name
\ttarget_lv LV name
\tforce suppress warning message and disable prompt, default True
Returns:
\tBoolean:
\t\tTrue if success
\t'tFalse in case of failure
"""
options = ""
if force:
options += " --pool-inactive"
if not target_vg or not target_lv:
_print("WARN: thin_trim requires target_vg and target_lv.")
return False
ret = self._check_device(target_vg, target_lv)
if not ret:
return False
ret = self._activate_device(target_vg, target_lv)
if not ret:
return False
device = self._get_device_path(target_vg, target_lv)
cmd = "thin_trim %s %s" % (device, options)
retcode = run(cmd, verbose=verbose)
if retcode != 0:
_print("WARN: Could not discard free pool space on device %s." % device)
return False
return True
def thin_delta(self, thin1, thin2, source_file=None, source_vg=None, source_lv=None, snapshot=False,
verbosity=False, verbose=True):
"""Print the differences in the mappings between two thin devices..
The arguments are:
\tsource_vg VG name
\tsource_lv LV name
\tthin1 numeric identificator of first thin volume
\tthin2 numeric identificator of second thin volume
\tsnapshot (Boolean/Int) Use metadata snapshot. If Int provided, specifies block number
\tverbosity Provide extra information on the mappings
Returns:
\tBoolean:
\t\tTrue if success
\t'tFalse in case of failure
"""
options = ""
if not source_file and (not source_vg or not source_lv):
_print("WARN: thin_delta requires either source_file OR source_vg and source_lv.")
return False
if not source_file:
ret = self._check_device(source_vg, source_lv)
if not ret:
return False
ret = self._activate_device(source_vg, source_lv)
if not ret:
return False
device = self._get_device_path(source_vg, source_lv)
else:
if not os.path.isfile(source_file):
_print("WARN: Source file is not a file.")
return False
device = source_file
if snapshot:
if isinstance(snapshot, bool):
options += "--metadata-snap "
elif isinstance(snapshot, int):
options += "--metadata-snap %s " % snapshot
else:
_print("WARN: Unknown snapshot value, use either Boolean or Int.")
return False
if verbosity:
options += "--verbose"
if self._get_dev_id(thin1, source_file, source_lv, source_vg) and \
self._get_dev_id(thin2, source_file, source_lv, source_vg):
cmd = "thin_delta %s --thin1 %s --thin2 %s %s" % (options, thin1, thin2, device)
retcode = run(cmd, verbose=verbose)
if retcode != 0:
_print("WARN: Could not get differences in mappings between two thin LVs.")
return False
else:
_print("WARN: Specified ID does not exist.")
return False
return True