blob: b7280f2c06ef51e7701197beb0a392ae48f95afa [file] [log] [blame]
#!/usr/bin/python3
# -*- python-mode -*-
'''
Copyright (C) 2020
Authors: Achilles Gaikwad <agaikwad@redhat.com>
Kenneth D'souza <kdsouza@redhat.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
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 <https://www.gnu.org/licenses/>.
'''
import multiprocessing as mp
import os
import signal
import sys
try:
import argparse
except ImportError:
print('%s: Failed to import argparse - make sure argparse is installed!'
% sys.argv[0])
sys.exit(1)
try:
import yaml
except ImportError:
print('%s: Failed to import yaml - make sure python3-pyyaml is installed!'
% sys.argv[0])
sys.exit(1)
BBOLD = '\033[1;30;47m' #Bold black text with white background.
ENDC = '\033[m' #Rest to defaults
def init_worker():
signal.signal(signal.SIGINT, signal.SIG_IGN)
# this function converts the info file to a dictionary format, sorta.
def file_to_dict(path):
client_info = {}
try:
with open(path) as f:
for line in f:
try:
(key, val) = line.split(':', 1)
client_info[key] = val.strip()
# FIXME: There has to be a better way of converting the info file to a dictionary.
except ValueError as reason:
if verbose:
print('Exception occured, %s' % reason)
if len(client_info) == 0 and verbose:
print("Provided %s file is not valid" %path)
return client_info
except OSError as reason:
if verbose:
print('%s' % reason)
# this function gets the paths from /proc/fs/nfsd/clients/
# returns a list of paths for each client which has nfs-share mounted.
def getpaths():
path = []
try:
dirs = os.listdir('/proc/fs/nfsd/clients/')
except OSError as reason:
exit('%s' % reason)
if len(dirs) !=0:
for i in dirs:
path.append('/proc/fs/nfsd/clients/' + i + '/states')
return (path)
else:
exit('Nothing to process')
# A single function to rule them all, in this function we gather all the data
# from already populated data_list and client_info.
def printer(data_list, argument):
client_info_path = data_list.pop()
client_info = file_to_dict(client_info_path)
for i in data_list:
for key in i:
inode = i[key]['superblock'].split(':')[-1]
# The ip address is quoted, so we dequote it.
try:
client_ip = client_info['address'][1:-1]
except:
client_ip = "N/A"
try:
# if the nfs-server reboots while the nfs-client holds the files open,
# the nfs-server would print the filename as '/'. For such instaces we
# print the output as disconnected dentry instead of '/'.
if(i[key]['filename']=='/'):
fname = 'disconnected dentry'
else:
fname = i[key]['filename'].split('/')[-1]
except KeyError:
# for older kernels which do not have the fname patch in kernel, they
# won't be able to see the fname field. Therefore post it as N/A.
fname = "N/A"
otype = i[key]['type']
try:
access = i[key]['access']
except:
access = ''
try:
deny = i[key]['deny']
except:
deny = ''
try:
hostname = client_info['name'].split()[-1].split('"')[0]
hostname = hostname.split('.')[0]
# if the hostname is too long, it messes up with the output being in columns,
# therefore we truncate the hostname followed by two '..' as suffix.
if len(hostname) > 20:
hostname = hostname[0:20] + '..'
except:
hostname = "N/A"
try:
clientid = client_info['clientid']
except:
clientid = "N/A"
try:
minorversion = "4." + client_info['minor version']
except:
minorversion = "N/A"
otype = i[key]['type']
# since some fields do not have deny column, we drop those if -t is either
# layout or lock.
drop = ['layout', 'lock']
# Printing the output this way instead of a single string which is concatenated
# this makes it better to quickly add more columns in future.
if(otype == argument.type or argument.type == 'all'):
print('%-13s' %inode, end='| ')
print('%-7s' %otype, end='| ')
if (argument.type not in drop):
print('%-7s' %access, end='| ')
if (argument.type not in drop and argument.type !='deleg'):
print('%-5s' %deny, end='| ')
if (argument.hostname == True):
print('%-22s' %hostname, end='| ')
else:
print('%-22s' %client_ip, end='| ')
if (argument.clientinfo == True) :
print('%-20s' %clientid, end='| ')
print('%-5s' %minorversion, end='| ')
print(fname)
def opener(path):
try:
with open(path, 'r') as nfsdata:
try:
data = yaml.load(nfsdata, Loader = yaml.BaseLoader)
if data is not None:
clientinfo = path.rsplit('/', 1)[0] + '/info'
data.append(clientinfo)
return data
except:
if verbose:
print("Exception occurred, Please make sure %s is a YAML file" %path)
except OSError as reason:
if verbose:
print('%s' % reason)
def print_cols(argument):
title_inode = 'Inode number'
title_otype = 'Type'
title_access = 'Access'
title_deny = 'Deny'
title_fname = 'Filename'
title_clientID = 'Client ID'
title_hostname = 'Hostname'
title_ip = 'ip address'
title_nfsvers = 'vers'
drop = ['lock', 'layout']
print(BBOLD, end='')
print('%-13s' %title_inode, end='| ')
print('%-7s' %title_otype, end='| ')
if (argument.type not in drop):
print('%-7s' %title_access, end='| ')
if (argument.type not in drop and argument.type !='deleg'):
print('%-5s' %title_deny, end='| ')
if (argument.hostname == True):
print('%-22s' %title_hostname, end='| ')
else:
print('%-22s' %title_ip, end='| ')
if (argument.clientinfo == True):
print('%-20s' %title_clientID, end='| ')
print('%-5s' %title_nfsvers, end='| ')
print(title_fname, end='')
print(ENDC)
def nfsd4_show():
parser = argparse.ArgumentParser(description = 'Parse the nfsd states and clientinfo files.')
parser.add_argument('-t', '--type', metavar = 'type', type = str, choices = ['open',
'deleg', 'lock', 'layout', 'all'],
default = 'all',
help = 'Input the type that you want to be printed: open, lock, deleg, layout, all')
parser.add_argument('--clientinfo', action = 'store_true',
help = 'output clients information, --hostname is implied.')
parser.add_argument('--hostname', action = 'store_true',
help = 'print hostname of client instead of its ip address. Longer hostnames are truncated.')
parser.add_argument('-v', '--verbose', action = 'store_true',
help = 'Verbose operation, show debug messages.')
parser.add_argument('-f', '--file', nargs='+', type = str, metavar='',
help = 'pass client states file, provided that info file resides in the same directory.')
parser.add_argument('-q', '--quiet', action = 'store_true',
help = 'don\'t print the header information')
args = parser.parse_args()
global verbose
verbose = False
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
if args.verbose:
verbose = True
if args.file:
paths = args.file
else:
paths = getpaths()
p = mp.Pool(mp.cpu_count(), init_worker)
try:
result = p.map(opener, paths)
### Drop None entries from list
final_result = list(filter(None, result))
p.close()
p.join()
if len(final_result) !=0 and not args.quiet:
print_cols(args)
for item in final_result:
printer(item, args)
except KeyboardInterrupt:
print('Caught KeyboardInterrupt, terminating workers')
p.terminate()
p.join()
if __name__ == "__main__":
nfsd4_show()