Skip to content

Commit 629c6ff

Browse files
committed
Add LTP fsstress test for NFS mounted filesystem
Add new test to validate filesystem operations over NFS using LTP fsstress. This test extends the existing io/disk/ltp_fsstress.py to support network filesystem testing. Test workflow: - Configures host and peer network interfaces with private IPs - Establishes SSH connection to peer via public/management IP - Sets up NFS server on peer (exportfs, rpcbind, firewalld) - Mounts NFS share on host from peer's private IP - Downloads and builds LTP from source - Runs fsstress with configurable parameters on NFS mount - Monitors dmesg and console logs for errors - Performs complete cleanup including session termination Signed-off-by: Vaishnavi Bhat <vaishnavi@linux.vnet.ibm.com>
1 parent 1ea4e7f commit 629c6ff

File tree

2 files changed

+443
-0
lines changed

2 files changed

+443
-0
lines changed

io/net/ltp_fsstress_nfs.py

Lines changed: 396 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,396 @@
1+
#!/usr/bin/env python
2+
3+
# This program is free software; you can redistribute it and/or modify
4+
# it under the terms of the GNU General Public License as published by
5+
# the Free Software Foundation; either version 2 of the License, or
6+
# (at your option) any later version.
7+
#
8+
# This program is distributed in the hope that it will be useful,
9+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11+
#
12+
# See LICENSE for more details.
13+
#
14+
# Copyright: 2026 IBM
15+
# Author: Vaishnavi Bhat <vaishnavi@linux.vnet.ibm.com>
16+
#
17+
# https://github.com/linux-test-project/ltp
18+
19+
"""
20+
LTP fsstress test on NFS mounted filesystem
21+
:avocado: tags=net,fs,privileged
22+
"""
23+
24+
import os
25+
import time
26+
from avocado import Test
27+
from avocado.utils import build
28+
from avocado.utils import dmesg
29+
from avocado.utils import process, archive
30+
from avocado.utils import distro
31+
from avocado.utils.software_manager.manager import SoftwareManager
32+
from avocado.utils.ssh import Session
33+
from avocado.utils.network.hosts import LocalHost, RemoteHost
34+
from avocado.utils.network.interfaces import NetworkInterface
35+
36+
37+
class LtpFsNfs(Test):
38+
'''
39+
Using LTP (Linux Test Project) fsstress to test NFS mounted filesystem
40+
'''
41+
42+
def setUp(self):
43+
'''
44+
Setup network interfaces, NFS server on peer, and mount NFS on host
45+
'''
46+
self.err_mesg = []
47+
48+
# Get test parameters
49+
self.host_interface = self.params.get('host_interface', default=None)
50+
self.host_ip = self.params.get('host_ip', default=None)
51+
self.peer_interface = self.params.get('peer_interface', default=None)
52+
self.peer_ip = self.params.get('peer_ip', default=None)
53+
self.peer_public_ip = self.params.get('peer_public_ip', default=None)
54+
self.peer_user = self.params.get('peer_user', default='root')
55+
self.peer_password = self.params.get('peer_password', default=None)
56+
self.netmask = self.params.get('netmask', default='255.255.255.0')
57+
58+
# NFS mount parameters
59+
self.nfs_mount_point = self.params.get('nfs_mount_point', default='/mnt/nfs')
60+
self.nfs_server_path = self.params.get('nfs_server_path', default='/mnt/nfssrc')
61+
62+
# fsstress parameters
63+
self.fsstress_count = self.params.get('fsstress_loop', default='0')
64+
self.n_val = self.params.get('n_val', default='200')
65+
self.p_val = self.params.get('p_val', default='200')
66+
67+
# Validate required parameters
68+
if not all([self.host_interface, self.host_ip, self.peer_ip,
69+
self.peer_public_ip, self.peer_password]):
70+
self.cancel("Missing required parameters: host_interface, host_ip, "
71+
"peer_ip, peer_public_ip, peer_password")
72+
73+
# Initialize localhost
74+
self.localhost = LocalHost()
75+
76+
# Check if host interface exists
77+
interfaces = os.listdir('/sys/class/net')
78+
if self.host_interface not in interfaces:
79+
self.cancel(f"Host interface {self.host_interface} not available")
80+
81+
# Install required packages
82+
smm = SoftwareManager()
83+
detected_distro = distro.detect()
84+
packages = ['gcc', 'make', 'automake', 'autoconf', 'nfs-utils']
85+
86+
if detected_distro.name == 'Ubuntu':
87+
packages.extend(['nfs-common', 'openssh-client', 'iputils-ping'])
88+
elif detected_distro.name in ['rhel', 'fedora', 'centos', 'redhat']:
89+
packages.extend(['openssh-clients', 'iputils'])
90+
else:
91+
packages.extend(['openssh', 'iputils'])
92+
93+
for package in packages:
94+
if not smm.check_installed(package) and not smm.install(package):
95+
self.cancel(f"{package} is needed for the test to be run")
96+
97+
# Configure host interface
98+
self.log.info(f"Configuring host interface {self.host_interface} with IP {self.host_ip}")
99+
self.host_networkinterface = NetworkInterface(self.host_interface, self.localhost)
100+
try:
101+
self.host_networkinterface.add_ipaddr(self.host_ip, self.netmask)
102+
self.host_networkinterface.bring_up()
103+
except Exception as e:
104+
self.log.info(f"Host interface configuration: {e}")
105+
106+
# Establish SSH connection to peer using public IP
107+
self.log.info(f"Connecting to peer at {self.peer_public_ip}")
108+
self.session = Session(self.peer_public_ip, user=self.peer_user,
109+
password=self.peer_password)
110+
if not self.session.connect():
111+
self.cancel("Failed to connect to peer machine")
112+
113+
# Initialize remote host with peer private IP for network operations
114+
self.remotehost = RemoteHost(self.peer_ip, self.peer_user,
115+
password=self.peer_password)
116+
117+
# Get peer interface by IP or use specified interface
118+
peer_iface = None
119+
if self.peer_interface:
120+
peer_iface = self.peer_interface
121+
else:
122+
# Try to get interface by IP address
123+
try:
124+
peer_iface = self.remotehost.get_interface_by_ipaddr(self.peer_ip).name
125+
except Exception:
126+
self.cancel("Could not determine peer interface. Please specify peer_interface in YAML")
127+
128+
# Configure peer interface with private IP
129+
if peer_iface:
130+
self.log.info(f"Configuring peer interface {peer_iface} with IP {self.peer_ip}")
131+
self.peer_networkinterface = NetworkInterface(peer_iface, self.remotehost)
132+
else:
133+
self.cancel("Peer interface not specified and could not be determined")
134+
try:
135+
self.peer_networkinterface.add_ipaddr(self.peer_ip, self.netmask)
136+
self.peer_networkinterface.bring_up()
137+
except Exception as e:
138+
self.log.info(f"Peer interface configuration: {e}")
139+
140+
# Ping check
141+
self.log.info(f"Performing ping check to {self.peer_ip}")
142+
ping_cmd = f"ping -c 5 {self.peer_ip}"
143+
if process.system(ping_cmd, shell=True, ignore_status=True) != 0:
144+
self.cancel(f"Ping to peer {self.peer_ip} failed")
145+
146+
# Setup NFS server on peer
147+
self.setup_nfs_server()
148+
149+
# Mount NFS on host
150+
self.mount_nfs()
151+
152+
# Download and build LTP
153+
self.log.info("Downloading and building LTP")
154+
url = "https://github.com/linux-test-project/ltp/archive/master.zip"
155+
tarball = self.fetch_asset("ltp-master.zip", locations=[url], expire='7d')
156+
archive.extract(tarball, self.teststmpdir)
157+
ltp_dir = os.path.join(self.teststmpdir, "ltp-master")
158+
os.chdir(ltp_dir)
159+
build.make(ltp_dir, extra_args='autotools')
160+
process.system('./configure', ignore_status=True)
161+
build.make(ltp_dir)
162+
build.make(ltp_dir, extra_args='install')
163+
self.fsstress_dir = os.path.join(ltp_dir, 'testcases/kernel/fs/fsstress')
164+
os.chdir(self.fsstress_dir)
165+
166+
# Clear dmesg
167+
dmesg.clear_dmesg()
168+
169+
def setup_nfs_server(self):
170+
"""
171+
Setup NFS server on peer machine:
172+
- Create NFS export directory
173+
- Start NFS server
174+
- Stop firewalld
175+
- Export the directory
176+
"""
177+
self.log.info("Setting up NFS server on peer")
178+
179+
# Create NFS export directory on peer
180+
cmd = f"mkdir -p {self.nfs_server_path}"
181+
result = self.session.cmd(cmd)
182+
if result.exit_status != 0:
183+
self.cancel(f"Failed to create NFS export directory on peer: {result.stderr}")
184+
185+
# Install NFS server packages on peer
186+
detected_distro = distro.detect()
187+
if detected_distro.name == 'Ubuntu':
188+
nfs_pkg_cmd = "apt-get update && apt-get install -y nfs-kernel-server"
189+
else:
190+
nfs_pkg_cmd = "yum install -y nfs-utils"
191+
192+
result = self.session.cmd(nfs_pkg_cmd)
193+
if result.exit_status != 0:
194+
self.log.warning(f"NFS package installation on peer: {result.stderr}")
195+
196+
# Start NFS server on peer
197+
self.log.info("Starting NFS server on peer")
198+
start_nfs_cmds = [
199+
"systemctl start nfs-server",
200+
"systemctl enable nfs-server",
201+
"systemctl start rpcbind",
202+
"systemctl enable rpcbind"
203+
]
204+
for cmd in start_nfs_cmds:
205+
result = self.session.cmd(cmd)
206+
if result.exit_status != 0:
207+
self.log.warning(f"Command '{cmd}' on peer: {result.stderr}")
208+
209+
# Stop firewalld on peer
210+
self.log.info("Stopping firewalld on peer")
211+
firewall_cmds = [
212+
"systemctl stop firewalld",
213+
"systemctl disable firewalld"
214+
]
215+
for cmd in firewall_cmds:
216+
result = self.session.cmd(cmd)
217+
# Ignore errors as firewalld might not be running
218+
219+
# Export the directory on peer
220+
self.log.info(f"Exporting {self.nfs_server_path} to {self.host_ip}")
221+
export_cmd = f"exportfs -o rw,sync,no_root_squash {self.host_ip}:{self.nfs_server_path}"
222+
result = self.session.cmd(export_cmd)
223+
if result.exit_status != 0:
224+
self.cancel(f"Failed to export NFS directory on peer: {result.stderr}")
225+
226+
# Verify export
227+
result = self.session.cmd("exportfs -v")
228+
self.log.info(f"NFS exports on peer:\n{result.stdout}")
229+
230+
def mount_nfs(self):
231+
"""
232+
Mount NFS filesystem on host
233+
"""
234+
self.log.info(f"Mounting NFS from {self.peer_ip}:{self.nfs_server_path} to {self.nfs_mount_point}")
235+
236+
# Create mount point on host
237+
cmd = f"mkdir -p {self.nfs_mount_point}"
238+
if process.system(cmd, shell=True, ignore_status=True) != 0:
239+
self.cancel(f"Failed to create mount point {self.nfs_mount_point}")
240+
241+
# Mount NFS
242+
mount_cmd = f"mount -t nfs {self.peer_ip}:{self.nfs_server_path} {self.nfs_mount_point}"
243+
if process.system(mount_cmd, shell=True, ignore_status=True) != 0:
244+
self.cancel(f"Failed to mount NFS from {self.peer_ip}:{self.nfs_server_path}")
245+
246+
# Verify mount
247+
verify_cmd = f"mount | grep {self.nfs_mount_point}"
248+
result = process.system_output(verify_cmd, shell=True, ignore_status=True)
249+
self.log.info(f"NFS mount verification:\n{result.decode('utf-8')}")
250+
251+
# Test write access
252+
test_file = os.path.join(self.nfs_mount_point, 'test_write')
253+
cmd = f"touch {test_file} && rm -f {test_file}"
254+
if process.system(cmd, shell=True, ignore_status=True) != 0:
255+
self.cancel(f"NFS mount {self.nfs_mount_point} is not writable")
256+
257+
self.log.info("NFS mount successful and writable")
258+
259+
def test_fsstress_run(self):
260+
'''
261+
Run LTP fsstress test on NFS mounted filesystem
262+
'''
263+
self.log.info("Starting fsstress test on NFS mount")
264+
arg = f"-d {self.nfs_mount_point} -n {self.n_val} -p {self.p_val} -r -l {self.fsstress_count}"
265+
self.log.info(f"fsstress arguments: {arg}")
266+
267+
# Clear dmesg before test
268+
dmesg.clear_dmesg()
269+
270+
# Run fsstress in background using nohup
271+
cmd = f"nohup ./fsstress {arg} &"
272+
self.log.info(f"Running command: {cmd}")
273+
process.run(cmd, shell=True, ignore_status=True)
274+
275+
# Get the PID of fsstress
276+
time.sleep(2)
277+
pid_cmd = "pgrep -f fsstress"
278+
try:
279+
pid_output = process.system_output(pid_cmd, shell=True, ignore_status=True)
280+
if pid_output:
281+
self.fsstress_pid = pid_output.decode('utf-8').strip().split()[0]
282+
self.log.info(f"fsstress running with PID: {self.fsstress_pid}")
283+
else:
284+
self.log.warning("Could not get fsstress PID")
285+
self.fsstress_pid = None
286+
except Exception as e:
287+
self.log.warning(f"Error getting fsstress PID: {e}")
288+
self.fsstress_pid = None
289+
290+
# Wait for fsstress to complete or monitor it
291+
if self.fsstress_count == '0':
292+
# Infinite loop - let it run for a reasonable time
293+
self.log.info("fsstress running in infinite loop mode, monitoring for 60 seconds")
294+
time.sleep(60)
295+
# Kill fsstress after monitoring period
296+
if self.fsstress_pid:
297+
kill_cmd = f"kill {self.fsstress_pid}"
298+
process.system(kill_cmd, shell=True, ignore_status=True)
299+
self.log.info("Stopped fsstress after monitoring period")
300+
else:
301+
# Wait for fsstress to complete
302+
self.log.info(f"Waiting for fsstress to complete {self.fsstress_count} loops")
303+
max_wait = int(self.fsstress_count) * 10 + 300 # Estimate wait time
304+
wait_time = 0
305+
while wait_time < max_wait:
306+
check_cmd = "pgrep -f fsstress"
307+
result = process.system(check_cmd, shell=True, ignore_status=True)
308+
if result != 0:
309+
self.log.info("fsstress completed")
310+
break
311+
time.sleep(10)
312+
wait_time += 10
313+
else:
314+
self.log.warning("fsstress did not complete in expected time, stopping it")
315+
if self.fsstress_pid:
316+
kill_cmd = f"kill {self.fsstress_pid}"
317+
process.system(kill_cmd, shell=True, ignore_status=True)
318+
319+
# Check dmesg for errors
320+
self.log.info("Checking dmesg for errors")
321+
cmd = "dmesg --level=err,crit,alert,emerg"
322+
dmesg_output = process.system_output(cmd, shell=True, ignore_status=True, sudo=False)
323+
if dmesg_output:
324+
dmesg_str = dmesg_output.decode('utf-8')
325+
self.log.warning(f"Errors found in dmesg:\n{dmesg_str}")
326+
# Check for NFS-related errors
327+
if 'nfs' in dmesg_str.lower() or 'rpc' in dmesg_str.lower():
328+
self.fail("NFS-related errors found in dmesg during fsstress test")
329+
else:
330+
self.log.info("No errors found in dmesg")
331+
332+
# Check console logs if available
333+
console_log = "/var/log/messages"
334+
if os.path.exists(console_log):
335+
self.log.info("Checking console logs for NFS errors")
336+
grep_cmd = f"grep -i 'nfs\\|rpc' {console_log} | tail -50"
337+
console_output = process.system_output(grep_cmd, shell=True, ignore_status=True)
338+
if console_output:
339+
self.log.info(f"Recent NFS/RPC messages in console:\n{console_output.decode('utf-8')}")
340+
341+
def tearDown(self):
342+
'''
343+
Cleanup: unmount NFS, stop NFS server on peer, cleanup network configuration
344+
'''
345+
self.log.info("Starting cleanup")
346+
347+
# Kill any remaining fsstress processes
348+
kill_cmd = "pkill -9 fsstress"
349+
process.system(kill_cmd, shell=True, ignore_status=True)
350+
351+
# Unmount NFS on host
352+
self.log.info(f"Unmounting NFS from {self.nfs_mount_point}")
353+
umount_cmd = f"umount -f {self.nfs_mount_point}"
354+
if process.system(umount_cmd, shell=True, ignore_status=True) != 0:
355+
self.log.warning(f"Failed to unmount {self.nfs_mount_point}")
356+
self.err_mesg.append(f"Failed to unmount {self.nfs_mount_point}")
357+
# Try lazy unmount
358+
lazy_umount = f"umount -l {self.nfs_mount_point}"
359+
process.system(lazy_umount, shell=True, ignore_status=True)
360+
361+
# Remove mount point
362+
rm_cmd = f"rmdir {self.nfs_mount_point}"
363+
process.system(rm_cmd, shell=True, ignore_status=True)
364+
365+
# Cleanup NFS server on peer
366+
if hasattr(self, 'session') and self.session:
367+
self.log.info("Cleaning up NFS server on peer")
368+
369+
# Unexport the directory
370+
unexport_cmd = f"exportfs -u {self.host_ip}:{self.nfs_server_path}"
371+
result = self.session.cmd(unexport_cmd)
372+
if result.exit_status != 0:
373+
self.log.warning(f"Failed to unexport NFS directory: {result.stderr}")
374+
375+
# Remove NFS export directory
376+
rm_nfs_dir = f"rm -rf {self.nfs_server_path}"
377+
self.session.cmd(rm_nfs_dir)
378+
379+
# Close SSH session
380+
self.session.quit()
381+
382+
# Close RemoteHost sessions
383+
if hasattr(self, 'remotehost') and self.remotehost:
384+
try:
385+
self.remotehost.remote_session.quit()
386+
except Exception as e:
387+
self.log.debug(f"Error closing remotehost session: {e}")
388+
389+
# Clear dmesg
390+
dmesg.clear_dmesg()
391+
392+
# Report any errors
393+
if self.err_mesg:
394+
self.log.warning(f"Test completed with errors: {self.err_mesg}")
395+
396+
# Made with AI Support

0 commit comments

Comments
 (0)