blob: 88f5297f93667b53cf4fc7a0653fda4b7ad80084 [file] [log] [blame]
#!/usr/bin/env python3
"""
# client_server.py
#
# Test fio's client/server mode.
#
# USAGE
# see python3 client_server.py --help
#
# EXAMPLES
# python3 t/client_server.py
# python3 t/client_server.py -f ./fio
#
# REQUIREMENTS
# Python 3.6
#
# This will start fio server instances listening on the interfaces below and
# will break if any ports are already occupied.
#
#
"""
import os
import sys
import time
import locale
import logging
import argparse
import tempfile
import subprocess
import configparser
from pathlib import Path
from fiotestlib import FioJobCmdTest, run_fio_tests
SERVER_LIST = [
",8765",
",8766",
",8767",
",8768",
]
PIDFILE_LIST = []
class ClientServerTest(FioJobCmdTest):
"""
Client/sever test class.
"""
def setup(self, parameters):
"""Setup a test."""
fio_args = [
f"--output={self.filenames['output']}",
f"--output-format={self.fio_opts['output-format']}",
]
for server in self.fio_opts['servers']:
option = f"--client={server['client']}"
fio_args.append(option)
fio_args.append(server['jobfile'])
super().setup(fio_args)
class ClientServerTestGlobalSingle(ClientServerTest):
"""
Client/sever test class.
One server connection only.
The job file may or may not have a global section.
"""
def check_result(self):
super().check_result()
config = configparser.ConfigParser(allow_no_value=True)
config.read(self.fio_opts['servers'][0]['jobfile'])
if not config.has_section('global'):
if len(self.json_data['global options']) > 0:
self.failure_reason = f"{self.failure_reason} non-empty 'global options' dictionary found with no global section in job file."
self.passed = False
return
if len(self.json_data['global options']) == 0:
self.failure_reason = f"{self.failure_reason} empty 'global options' dictionary found with no global section in job file."
self.passed = False
# Now make sure job file global section matches 'global options'
# in JSON output
job_file_global = dict(config['global'])
for key, value in job_file_global.items():
if value is None:
job_file_global[key] = ""
if job_file_global != self.json_data['global options']:
self.failure_reason = f"{self.failure_reason} 'global options' dictionary does not match global section in job file."
self.passed = False
class ClientServerTestGlobalMultiple(ClientServerTest):
"""
Client/sever test class.
Multiple server connections.
Job files may or may not have a global section.
"""
def check_result(self):
super().check_result()
#
# For each job file, check if it has a global section
# If so, make sure the 'global options' array has
# as element for it.
# At the end, make sure the total number of elements matches the number
# of job files with global sections.
#
global_sections = 0
for server in self.fio_opts['servers']:
config = configparser.ConfigParser(allow_no_value=True)
config.read(server['jobfile'])
if not config.has_section('global'):
continue
global_sections += 1
# this can only parse one server spec format
[hostname, port] = server['client'].split(',')
match = None
for global_opts in self.json_data['global options']:
if 'hostname' not in global_opts:
continue
if 'port' not in global_opts:
continue
if global_opts['hostname'] == hostname and int(global_opts['port']) == int(port):
match = global_opts
break
if not match:
self.failure_reason = f"{self.failure_reason} matching 'global options' element not found for {hostname}, {port}."
self.passed = False
continue
del match['hostname']
del match['port']
# Now make sure job file global section matches 'global options'
# in JSON output
job_file_global = dict(config['global'])
for key, value in job_file_global.items():
if value is None:
job_file_global[key] = ""
if job_file_global != match:
self.failure_reason += " 'global options' dictionary does not match global section in job file."
self.passed = False
else:
logging.debug("Job file global section matches 'global options' array element %s", server['client'])
if global_sections != len(self.json_data['global options']):
self.failure_reason = f"{self.failure_reason} mismatched number of elements in 'global options' array."
self.passed = False
else:
logging.debug("%d elements in global options array as expected", global_sections)
class ClientServerTestAllClientsLat(ClientServerTest):
"""
Client/sever test class.
Make sure the "All clients" job has latency percentile data.
Assumes that a job named 'test' is run with no global section.
Only check read data.
"""
def check_result(self):
super().check_result()
config = configparser.ConfigParser(allow_no_value=True)
config.read(self.fio_opts['servers'][0]['jobfile'])
lats = { 'clat': True, 'lat': False, 'slat': False }
for key in lats:
opt = f"{key}_percentiles"
if opt in config.options('test'):
lats[key] = config.getboolean('test', opt)
logging.debug("%s set to %s", opt, lats[key])
all_clients = None
client_stats = self.json_data['client_stats']
for client in client_stats:
if client['jobname'] == "All clients":
all_clients = client
break
if not all_clients:
self.failure_reason = f"{self.failure_reason} Could not find 'All clients' output"
self.passed = False
for key, value in lats.items():
if value:
if 'percentile' not in all_clients['read'][f"{key}_ns"]:
self.failure_reason += f" {key} percentiles not found"
self.passed = False
break
logging.debug("%s percentiles found as expected", key)
else:
if 'percentile' in all_clients['read'][f"{key}_ns"]:
self.failure_reason += f" {key} percentiles found unexpectedly"
self.passed = False
break
logging.debug("%s percentiles appropriately not found", key)
TEST_LIST = [
{ # Smoke test
"test_id": 1,
"fio_opts": {
"output-format": "json",
"servers": [
{
"client" : 0, # index into the SERVER_LIST array
"jobfile": "test01.fio",
},
]
},
"test_class": ClientServerTest,
},
{ # try another client
"test_id": 2,
"fio_opts": {
"output-format": "json",
"servers": [
{
"client" : 1,
"jobfile": "test01.fio",
},
]
},
"test_class": ClientServerTest,
},
{ # single client global section
"test_id": 3,
"fio_opts": {
"output-format": "json",
"servers": [
{
"client" : 2,
"jobfile": "test01.fio",
},
]
},
"test_class": ClientServerTestGlobalSingle,
},
{ # single client no global section
"test_id": 4,
"fio_opts": {
"output-format": "json",
"servers": [
{
"client" : 3,
"jobfile": "test04-noglobal.fio",
},
]
},
"test_class": ClientServerTestGlobalSingle,
},
{ # multiple clients, some with global, some without
"test_id": 5,
"fio_opts": {
"output-format": "json",
"servers": [
{
"client" : 0,
"jobfile": "test04-noglobal.fio",
},
{
"client" : 1,
"jobfile": "test01.fio",
},
{
"client" : 2,
"jobfile": "test04-noglobal.fio",
},
{
"client" : 3,
"jobfile": "test01.fio",
},
]
},
"test_class": ClientServerTestGlobalMultiple,
},
{ # multiple clients, all with global sections
"test_id": 6,
"fio_opts": {
"output-format": "json",
"servers": [
{
"client" : 0,
"jobfile": "test01.fio",
},
{
"client" : 1,
"jobfile": "test01.fio",
},
{
"client" : 2,
"jobfile": "test01.fio",
},
{
"client" : 3,
"jobfile": "test01.fio",
},
]
},
"test_class": ClientServerTestGlobalMultiple,
},
{ # Enable submission latency
"test_id": 7,
"fio_opts": {
"output-format": "json",
"servers": [
{
"client" : 0,
"jobfile": "test07-slat.fio",
},
{
"client" : 1,
"jobfile": "test07-slat.fio",
},
]
},
"test_class": ClientServerTestAllClientsLat,
},
{ # Enable completion latency
"test_id": 8,
"fio_opts": {
"output-format": "json",
"servers": [
{
"client" : 0,
"jobfile": "test08-clat.fio",
},
{
"client" : 1,
"jobfile": "test08-clat.fio",
},
]
},
"test_class": ClientServerTestAllClientsLat,
},
{ # Enable total latency
"test_id": 9,
"fio_opts": {
"output-format": "json",
"servers": [
{
"client" : 0,
"jobfile": "test09-lat.fio",
},
{
"client" : 1,
"jobfile": "test09-lat.fio",
},
]
},
"test_class": ClientServerTestAllClientsLat,
},
{ # Disable completion latency
"test_id": 10,
"fio_opts": {
"output-format": "json",
"servers": [
{
"client" : 0,
"jobfile": "test10-noclat.fio",
},
{
"client" : 1,
"jobfile": "test10-noclat.fio",
},
]
},
"test_class": ClientServerTestAllClientsLat,
},
{ # Enable submission, completion, total latency
"test_id": 11,
"fio_opts": {
"output-format": "json",
"servers": [
{
"client" : 0,
"jobfile": "test11-alllat.fio",
},
{
"client" : 1,
"jobfile": "test11-alllat.fio",
},
]
},
"test_class": ClientServerTestAllClientsLat,
},
]
def parse_args():
"""Parse command-line arguments."""
parser = argparse.ArgumentParser()
parser.add_argument('-d', '--debug', help='Enable debug messages', action='store_true')
parser.add_argument('-f', '--fio', help='path to file executable (e.g., ./fio)')
parser.add_argument('-a', '--artifact-root', help='artifact root directory')
parser.add_argument('-s', '--skip', nargs='+', type=int,
help='list of test(s) to skip')
parser.add_argument('-o', '--run-only', nargs='+', type=int,
help='list of test(s) to run, skipping all others')
args = parser.parse_args()
return args
def start_servers(fio_path, servers=SERVER_LIST):
"""Start servers for our tests."""
for server in servers:
tmpfile = tempfile.mktemp()
cmd = [fio_path, f"--server={server}", f"--daemonize={tmpfile}"]
cmd_result = subprocess.run(cmd, capture_output=True, check=False,
encoding=locale.getpreferredencoding())
if cmd_result.returncode != 0:
logging.error("Unable to start server on %s: %s", server, cmd_result.stderr)
return False
logging.debug("Started server %s", server)
PIDFILE_LIST.append(tmpfile)
return True
def stop_servers(pidfiles=PIDFILE_LIST):
"""Stop running fio server invocations."""
for pidfile in pidfiles:
with open(pidfile, "r", encoding=locale.getpreferredencoding()) as file:
pid = file.read().strip()
cmd = ["kill", f"{pid}"]
cmd_result = subprocess.run(cmd, capture_output=True, check=False,
encoding=locale.getpreferredencoding())
if cmd_result.returncode != 0:
logging.error("Unable to kill server with PID %s: %s", pid, cmd_result.stderr)
return False
logging.debug("Sent stop signal to PID %s", pid)
return True
def main():
"""Run tests for fio's client/server mode."""
args = parse_args()
if args.debug:
logging.basicConfig(level=logging.DEBUG)
else:
logging.basicConfig(level=logging.INFO)
artifact_root = args.artifact_root if args.artifact_root else \
f"client_server-test-{time.strftime('%Y%m%d-%H%M%S')}"
os.mkdir(artifact_root)
print(f"Artifact directory is {artifact_root}")
if args.fio:
fio_path = str(Path(args.fio).absolute())
else:
fio_path = os.path.join(os.path.dirname(__file__), '../fio')
print(f"fio path is {fio_path}")
if not start_servers(fio_path):
sys.exit(1)
print("Servers started")
job_path = os.path.join(os.path.dirname(__file__), "client_server")
for test in TEST_LIST:
opts = test['fio_opts']
for server in opts['servers']:
server['client'] = SERVER_LIST[server['client']]
server['jobfile'] = os.path.join(job_path, server['jobfile'])
test_env = {
'fio_path': fio_path,
'fio_root': str(Path(__file__).absolute().parent.parent),
'artifact_root': artifact_root,
'basename': 'client_server',
}
_, failed, _ = run_fio_tests(TEST_LIST, test_env, args)
stop_servers()
sys.exit(failed)
if __name__ == '__main__':
main()