#!/usr/bin/env python
# -*- python-mode -*-
"""Parse /proc/self/mountstats and display it in human readable form
"""

__copyright__ = """
Copyright (C) 2005, Chuck Lever <cel@netapp.com>

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2 as
published by the Free Software Foundation.

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, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
MA 02110-1301 USA
"""

import sys, os, time

Mountstats_version = '0.2'

def difference(x, y):
    """Used for a map() function
    """
    return x - y

class DeviceData:
    """DeviceData objects provide methods for parsing and displaying
    data for a single mount grabbed from /proc/self/mountstats
    """
    def __init__(self):
        self.__nfs_data = dict()
        self.__rpc_data = dict()
        self.__rpc_data['ops'] = []

    def __parse_nfs_line(self, words):
        if words[0] == 'device':
            self.__nfs_data['export'] = words[1]
            self.__nfs_data['mountpoint'] = words[4]
            self.__nfs_data['fstype'] = words[7]
            if words[7].find('nfs') != -1:
                self.__nfs_data['statvers'] = words[8]
        elif 'nfs' in words or 'nfs4' in words:
            self.__nfs_data['export'] = words[0]
            self.__nfs_data['mountpoint'] = words[3]
            self.__nfs_data['fstype'] = words[6]
            if words[6].find('nfs') != -1:
                self.__nfs_data['statvers'] = words[7]
        elif words[0] == 'age:':
            self.__nfs_data['age'] = long(words[1])
        elif words[0] == 'opts:':
            self.__nfs_data['mountoptions'] = ''.join(words[1:]).split(',')
        elif words[0] == 'caps:':
            self.__nfs_data['servercapabilities'] = ''.join(words[1:]).split(',')
        elif words[0] == 'nfsv4:':
            self.__nfs_data['nfsv4flags'] = ''.join(words[1:]).split(',')
        elif words[0] == 'sec:':
            keys = ''.join(words[1:]).split(',')
            self.__nfs_data['flavor'] = int(keys[0].split('=')[1])
            self.__nfs_data['pseudoflavor'] = 0
            if self.__nfs_data['flavor'] == 6:
                self.__nfs_data['pseudoflavor'] = int(keys[1].split('=')[1])
        elif words[0] == 'events:':
            self.__nfs_data['inoderevalidates'] = int(words[1])
            self.__nfs_data['dentryrevalidates'] = int(words[2])
            self.__nfs_data['datainvalidates'] = int(words[3])
            self.__nfs_data['attrinvalidates'] = int(words[4])
            self.__nfs_data['syncinodes'] = int(words[5])
            self.__nfs_data['vfsopen'] = int(words[6])
            self.__nfs_data['vfslookup'] = int(words[7])
            self.__nfs_data['vfspermission'] = int(words[8])
            self.__nfs_data['vfsreadpage'] = int(words[9])
            self.__nfs_data['vfsreadpages'] = int(words[10])
            self.__nfs_data['vfswritepage'] = int(words[11])
            self.__nfs_data['vfswritepages'] = int(words[12])
            self.__nfs_data['vfsreaddir'] = int(words[13])
            self.__nfs_data['vfsflush'] = int(words[14])
            self.__nfs_data['vfsfsync'] = int(words[15])
            self.__nfs_data['vfslock'] = int(words[16])
            self.__nfs_data['vfsrelease'] = int(words[17])
            self.__nfs_data['setattrtrunc'] = int(words[18])
            self.__nfs_data['extendwrite'] = int(words[19])
            self.__nfs_data['sillyrenames'] = int(words[20])
            self.__nfs_data['shortreads'] = int(words[21])
            self.__nfs_data['shortwrites'] = int(words[22])
            self.__nfs_data['delay'] = int(words[23])
        elif words[0] == 'bytes:':
            self.__nfs_data['normalreadbytes'] = long(words[1])
            self.__nfs_data['normalwritebytes'] = long(words[2])
            self.__nfs_data['directreadbytes'] = long(words[3])
            self.__nfs_data['directwritebytes'] = long(words[4])
            self.__nfs_data['serverreadbytes'] = long(words[5])
            self.__nfs_data['serverwritebytes'] = long(words[6])

    def __parse_rpc_line(self, words):
        if words[0] == 'RPC':
            self.__rpc_data['statsvers'] = float(words[3])
            self.__rpc_data['programversion'] = words[5]
        elif words[0] == 'xprt:':
            self.__rpc_data['protocol'] = words[1]
            if words[1] == 'udp':
                self.__rpc_data['port'] = int(words[2])
                self.__rpc_data['bind_count'] = int(words[3])
                self.__rpc_data['rpcsends'] = int(words[4])
                self.__rpc_data['rpcreceives'] = int(words[5])
                self.__rpc_data['badxids'] = int(words[6])
                self.__rpc_data['inflightsends'] = long(words[7])
                self.__rpc_data['backlogutil'] = long(words[8])
            elif words[1] == 'tcp':
                self.__rpc_data['port'] = words[2]
                self.__rpc_data['bind_count'] = int(words[3])
                self.__rpc_data['connect_count'] = int(words[4])
                self.__rpc_data['connect_time'] = int(words[5])
                self.__rpc_data['idle_time'] = int(words[6])
                self.__rpc_data['rpcsends'] = int(words[7])
                self.__rpc_data['rpcreceives'] = int(words[8])
                self.__rpc_data['badxids'] = int(words[9])
                self.__rpc_data['inflightsends'] = long(words[10])
                self.__rpc_data['backlogutil'] = int(words[11])
            elif words[1] == 'rdma':
                self.__rpc_data['port'] = words[2]
                self.__rpc_data['bind_count'] = int(words[3])
                self.__rpc_data['connect_count'] = int(words[4])
                self.__rpc_data['connect_time'] = int(words[5])
                self.__rpc_data['idle_time'] = int(words[6])
                self.__rpc_data['rpcsends'] = int(words[7])
                self.__rpc_data['rpcreceives'] = int(words[8])
                self.__rpc_data['badxids'] = int(words[9])
                self.__rpc_data['backlogutil'] = int(words[10])
                self.__rpc_data['read_chunks'] = int(words[11])
                self.__rpc_data['write_chunks'] = int(words[12])
                self.__rpc_data['reply_chunks'] = int(words[13])
                self.__rpc_data['total_rdma_req'] = int(words[14])
                self.__rpc_data['total_rdma_rep'] = int(words[15])
                self.__rpc_data['pullup'] = int(words[16])
                self.__rpc_data['fixup'] = int(words[17])
                self.__rpc_data['hardway'] = int(words[18])
                self.__rpc_data['failed_marshal'] = int(words[19])
                self.__rpc_data['bad_reply'] = int(words[20])
        elif words[0] == 'per-op':
            self.__rpc_data['per-op'] = words
        else:
            op = words[0][:-1]
            self.__rpc_data['ops'] += [op]
            self.__rpc_data[op] = [long(word) for word in words[1:]]

    def parse_stats(self, lines):
        """Turn a list of lines from a mount stat file into a 
        dictionary full of stats, keyed by name
        """
        found = False
        for line in lines:
            words = line.split()
            if len(words) == 0:
                continue
            if (not found and words[0] != 'RPC'):
                self.__parse_nfs_line(words)
                continue

            found = True
            self.__parse_rpc_line(words)

    def is_nfs_mountpoint(self):
        """Return True if this is an NFS or NFSv4 mountpoint,
        otherwise return False
        """
        if self.__nfs_data['fstype'] == 'nfs':
            return True
        elif self.__nfs_data['fstype'] == 'nfs4':
            return True
        return False

    def display_nfs_options(self):
        """Pretty-print the NFS options
        """
        print 'Stats for %s mounted on %s:' % \
            (self.__nfs_data['export'], self.__nfs_data['mountpoint'])

        print '  NFS mount options: %s' % ','.join(self.__nfs_data['mountoptions'])
        print '  NFS server capabilities: %s' % ','.join(self.__nfs_data['servercapabilities'])
        if self.__nfs_data.has_key('nfsv4flags'):
            print '  NFSv4 capability flags: %s' % ','.join(self.__nfs_data['nfsv4flags'])
        if self.__nfs_data.has_key('pseudoflavor'):
            print '  NFS security flavor: %d  pseudoflavor: %d' % \
                (self.__nfs_data['flavor'], self.__nfs_data['pseudoflavor'])
        else:
            print '  NFS security flavor: %d' % self.__nfs_data['flavor']

    def display_nfs_events(self):
        """Pretty-print the NFS event counters
        """
        print
        print 'Cache events:'
        print '  data cache invalidated %d times' % self.__nfs_data['datainvalidates']
        print '  attribute cache invalidated %d times' % self.__nfs_data['attrinvalidates']
        print '  inodes synced %d times' % self.__nfs_data['syncinodes']
        print
        print 'VFS calls:'
        print '  VFS requested %d inode revalidations' % self.__nfs_data['inoderevalidates']
        print '  VFS requested %d dentry revalidations' % self.__nfs_data['dentryrevalidates']
        print
        print '  VFS called nfs_readdir() %d times' % self.__nfs_data['vfsreaddir']
        print '  VFS called nfs_lookup() %d times' % self.__nfs_data['vfslookup']
        print '  VFS called nfs_permission() %d times' % self.__nfs_data['vfspermission']
        print '  VFS called nfs_file_open() %d times' % self.__nfs_data['vfsopen']
        print '  VFS called nfs_file_flush() %d times' % self.__nfs_data['vfsflush']
        print '  VFS called nfs_lock() %d times' % self.__nfs_data['vfslock']
        print '  VFS called nfs_fsync() %d times' % self.__nfs_data['vfsfsync']
        print '  VFS called nfs_file_release() %d times' % self.__nfs_data['vfsrelease']
        print
        print 'VM calls:'
        print '  VFS called nfs_readpage() %d times' % self.__nfs_data['vfsreadpage']
        print '  VFS called nfs_readpages() %d times' % self.__nfs_data['vfsreadpages']
        print '  VFS called nfs_writepage() %d times' % self.__nfs_data['vfswritepage']
        print '  VFS called nfs_writepages() %d times' % self.__nfs_data['vfswritepages']
        print
        print 'Generic NFS counters:'
        print '  File size changing operations:'
        print '    truncating SETATTRs: %d  extending WRITEs: %d' % \
            (self.__nfs_data['setattrtrunc'], self.__nfs_data['extendwrite'])
        print '  %d silly renames' % self.__nfs_data['sillyrenames']
        print '  short reads: %d  short writes: %d' % \
            (self.__nfs_data['shortreads'], self.__nfs_data['shortwrites'])
        print '  NFSERR_DELAYs from server: %d' % self.__nfs_data['delay']

    def display_nfs_bytes(self):
        """Pretty-print the NFS event counters
        """
        print
        print 'NFS byte counts:'
        print '  applications read %d bytes via read(2)' % self.__nfs_data['normalreadbytes']
        print '  applications wrote %d bytes via write(2)' % self.__nfs_data['normalwritebytes']
        print '  applications read %d bytes via O_DIRECT read(2)' % self.__nfs_data['directreadbytes']
        print '  applications wrote %d bytes via O_DIRECT write(2)' % self.__nfs_data['directwritebytes']
        print '  client read %d bytes via NFS READ' % self.__nfs_data['serverreadbytes']
        print '  client wrote %d bytes via NFS WRITE' % self.__nfs_data['serverwritebytes']

    def display_rpc_generic_stats(self):
        """Pretty-print the generic RPC stats
        """
        sends = self.__rpc_data['rpcsends']

        print
        print 'RPC statistics:'

        print '  %d RPC requests sent, %d RPC replies received (%d XIDs not found)' % \
            (sends, self.__rpc_data['rpcreceives'], self.__rpc_data['badxids'])
        if sends != 0:
            print '  average backlog queue length: %d' % \
                (float(self.__rpc_data['backlogutil']) / sends)

    def display_rpc_op_stats(self):
        """Pretty-print the per-op stats
        """
        sends = self.__rpc_data['rpcsends']

        # XXX: these should be sorted by 'count'
        print
        for op in self.__rpc_data['ops']:
            stats = self.__rpc_data[op]
            count = stats[0]
            retrans = stats[1] - count
            if count != 0:
                print '%s:' % op
                print '\t%d ops (%d%%)' % \
                    (count, ((count * 100) / sends)),
                print '\t%d retrans (%d%%)' % (retrans, ((retrans * 100) / count)),
                print '\t%d major timeouts' % stats[2]
                print '\tavg bytes sent per op: %d\tavg bytes received per op: %d' % \
                    (stats[3] / count, stats[4] / count)
                print '\tbacklog wait: %f' % (float(stats[5]) / count),
                print '\tRTT: %f' % (float(stats[6]) / count),
                print '\ttotal execute time: %f (milliseconds)' % \
                    (float(stats[7]) / count)

    def compare_iostats(self, old_stats):
        """Return the difference between two sets of stats
        """
        result = DeviceData()

        # copy self into result
        for key, value in self.__nfs_data.iteritems():
            result.__nfs_data[key] = value
        for key, value in self.__rpc_data.iteritems():
            result.__rpc_data[key] = value

        # compute the difference of each item in the list
        # note the copy loop above does not copy the lists, just
        # the reference to them.  so we build new lists here
        # for the result object.
        for op in result.__rpc_data['ops']:
            result.__rpc_data[op] = map(difference, self.__rpc_data[op], old_stats.__rpc_data[op])

        # update the remaining keys we care about
        result.__rpc_data['rpcsends'] -= old_stats.__rpc_data['rpcsends']
        result.__rpc_data['backlogutil'] -= old_stats.__rpc_data['backlogutil']
        result.__nfs_data['serverreadbytes'] -= old_stats.__nfs_data['serverreadbytes']
        result.__nfs_data['serverwritebytes'] -= old_stats.__nfs_data['serverwritebytes']

        return result

    def display_iostats(self, sample_time):
        """Display NFS and RPC stats in an iostat-like way
        """
        sends = float(self.__rpc_data['rpcsends'])
        if sample_time == 0:
            sample_time = float(self.__nfs_data['age'])

        print
        print '%s mounted on %s:' % \
            (self.__nfs_data['export'], self.__nfs_data['mountpoint'])

        print '\top/s\trpc bklog'
        print '\t%.2f' % (sends / sample_time), 
        if sends != 0:
            print '\t%.2f' % \
                ((float(self.__rpc_data['backlogutil']) / sends) / sample_time)
        else:
            print '\t0.00'

        # reads:  ops/s, kB/s, avg rtt, and avg exe
        # XXX: include avg xfer size and retransmits?
        read_rpc_stats = self.__rpc_data['READ']
        ops = float(read_rpc_stats[0])
        kilobytes = float(self.__nfs_data['serverreadbytes']) / 1024
        rtt = float(read_rpc_stats[6])
        exe = float(read_rpc_stats[7])

        print '\treads:\tops/s\t\tkB/s\t\tavg RTT (ms)\tavg exe (ms)'
        print '\t\t%.2f' % (ops / sample_time),
        print '\t\t%.2f' % (kilobytes / sample_time),
        if ops != 0:
            print '\t\t%.2f' % (rtt / ops),
            print '\t\t%.2f' % (exe / ops)
        else:
            print '\t\t0.00',
            print '\t\t0.00'

        # writes:  ops/s, kB/s, avg rtt, and avg exe
        # XXX: include avg xfer size and retransmits?
        write_rpc_stats = self.__rpc_data['WRITE']
        ops = float(write_rpc_stats[0])
        kilobytes = float(self.__nfs_data['serverwritebytes']) / 1024
        rtt = float(write_rpc_stats[6])
        exe = float(write_rpc_stats[7])

        print '\twrites:\tops/s\t\tkB/s\t\tavg RTT (ms)\tavg exe (ms)'
        print '\t\t%.2f' % (ops / sample_time),
        print '\t\t%.2f' % (kilobytes / sample_time),
        if ops != 0:
            print '\t\t%.2f' % (rtt / ops),
            print '\t\t%.2f' % (exe / ops)
        else:
            print '\t\t0.00',
            print '\t\t0.00'

def parse_stats_file(filename):
    """pop the contents of a mountstats file into a dictionary,
    keyed by mount point.  each value object is a list of the
    lines in the mountstats file corresponding to the mount
    point named in the key.
    """
    ms_dict = dict()
    key = ''

    f = file(filename)
    for line in f.readlines():
        words = line.split()
        if len(words) == 0:
            continue
        if words[0] == 'device':
            key = words[4]
            new = [ line.strip() ]
        elif 'nfs' in words or 'nfs4' in words:
            key = words[3]
            new = [ line.strip() ]
        else:
            new += [ line.strip() ]
        ms_dict[key] = new
    f.close

    return ms_dict

def print_mountstats_help(name):
    print 'usage: %s [ options ] <mount point>' % name
    print
    print ' Version %s' % Mountstats_version
    print
    print ' Display NFS client per-mount statistics.'
    print
    print '  --version    display the version of this command'
    print '  --nfs        display only the NFS statistics'
    print '  --rpc        display only the RPC statistics'
    print '  --start      sample and save statistics'
    print '  --end        resample statistics and compare them with saved'
    print

def mountstats_command():
    """Mountstats command
    """
    mountpoints = []
    nfs_only = False
    rpc_only = False

    for arg in sys.argv:
        if arg in ['-h', '--help', 'help', 'usage']:
            print_mountstats_help(prog)
            return

        if arg in ['-v', '--version', 'version']:
            print '%s version %s' % (sys.argv[0], Mountstats_version)
            sys.exit(0)

        if arg in ['-n', '--nfs']:
            nfs_only = True
            continue

        if arg in ['-r', '--rpc']:
            rpc_only = True
            continue

        if arg in ['-s', '--start']:
            raise Exception, 'Sampling is not yet implemented'

        if arg in ['-e', '--end']:
            raise Exception, 'Sampling is not yet implemented'

        if arg == sys.argv[0]:
            continue

        mountpoints += [arg]

    if mountpoints == []:
        print_mountstats_help(prog)
        return

    if rpc_only == True and nfs_only == True:
        print_mountstats_help(prog)
        return

    mountstats = parse_stats_file('/proc/self/mountstats')

    for mp in mountpoints:
        if mp not in mountstats:
            print 'Statistics for mount point %s not found' % mp
            continue

        stats = DeviceData()
        stats.parse_stats(mountstats[mp])

        if not stats.is_nfs_mountpoint():
            print 'Mount point %s exists but is not an NFS mount' % mp
            continue

        if nfs_only:
           stats.display_nfs_options()
           stats.display_nfs_events()
           stats.display_nfs_bytes()
        elif rpc_only:
           stats.display_rpc_generic_stats()
           stats.display_rpc_op_stats()
        else:
           stats.display_nfs_options()
           stats.display_nfs_bytes()
           stats.display_rpc_generic_stats()
           stats.display_rpc_op_stats()

def print_nfsstat_help(name):
    print 'usage: %s [ options ]' % name
    print
    print ' Version %s' % Mountstats_version
    print
    print ' nfsstat-like program that uses NFS client per-mount statistics.'
    print

def nfsstat_command():
    print_nfsstat_help(prog)

def print_iostat_help(name):
    print 'usage: %s [ <interval> [ <count> ] ] [ <mount point> ] ' % name
    print
    print ' Version %s' % Mountstats_version
    print
    print ' iostat-like program to display NFS client per-mount statistics.'
    print
    print ' The <interval> parameter specifies the amount of time in seconds between'
    print ' each report.  The first report contains statistics for the time since each'
    print ' file system was mounted.  Each subsequent report contains statistics'
    print ' collected during the interval since the previous report.'
    print
    print ' If the <count> parameter is specified, the value of <count> determines the'
    print ' number of reports generated at <interval> seconds apart.  If the interval'
    print ' parameter is specified without the <count> parameter, the command generates'
    print ' reports continuously.'
    print
    print ' If one or more <mount point> names are specified, statistics for only these'
    print ' mount points will be displayed.  Otherwise, all NFS mount points on the'
    print ' client are listed.'
    print

def print_iostat_summary(old, new, devices, time):
    for device in devices:
        stats = DeviceData()
        stats.parse_stats(new[device])
        if not old:
            stats.display_iostats(time)
        else:
            old_stats = DeviceData()
            old_stats.parse_stats(old[device])
            diff_stats = stats.compare_iostats(old_stats)
            diff_stats.display_iostats(time)

def iostat_command():
    """iostat-like command for NFS mount points
    """
    mountstats = parse_stats_file('/proc/self/mountstats')
    devices = []
    interval_seen = False
    count_seen = False

    for arg in sys.argv:
        if arg in ['-h', '--help', 'help', 'usage']:
            print_iostat_help(prog)
            return

        if arg in ['-v', '--version', 'version']:
            print '%s version %s' % (sys.argv[0], Mountstats_version)
            return

        if arg == sys.argv[0]:
            continue

        if arg in mountstats:
            devices += [arg]
        elif not interval_seen:
            interval = int(arg)
            if interval > 0:
                interval_seen = True
            else:
                print 'Illegal <interval> value'
                return
        elif not count_seen:
            count = int(arg)
            if count > 0:
                count_seen = True
            else:
                print 'Illegal <count> value'
                return

    # make certain devices contains only NFS mount points
    if len(devices) > 0:
        check = []
        for device in devices:
            stats = DeviceData()
            stats.parse_stats(mountstats[device])
            if stats.is_nfs_mountpoint():
                check += [device]
        devices = check
    else:
        for device, descr in mountstats.iteritems():
            stats = DeviceData()
            stats.parse_stats(descr)
            if stats.is_nfs_mountpoint():
                devices += [device]
    if len(devices) == 0:
        print 'No NFS mount points were found'
        return

    old_mountstats = None
    sample_time = 0

    if not interval_seen:
        print_iostat_summary(old_mountstats, mountstats, devices, sample_time)
        return

    if count_seen:
        while count != 0:
            print_iostat_summary(old_mountstats, mountstats, devices, sample_time)
            old_mountstats = mountstats
            time.sleep(interval)
            sample_time = interval
            mountstats = parse_stats_file('/proc/self/mountstats')
            count -= 1
    else: 
        while True:
            print_iostat_summary(old_mountstats, mountstats, devices, sample_time)
            old_mountstats = mountstats
            time.sleep(interval)
            sample_time = interval
            mountstats = parse_stats_file('/proc/self/mountstats')

#
# Main
#
prog = os.path.basename(sys.argv[0])

try:
    if prog == 'mountstats':
        mountstats_command()
    elif prog == 'ms-nfsstat':
        nfsstat_command()
    elif prog == 'ms-iostat':
        iostat_command()
except KeyboardInterrupt:
    print 'Caught ^C... exiting'
    sys.exit(1)

sys.exit(0)
