#!/usr/bin/python
import logging
import optparse
import time

import isi.nfs.nfsstat.client.nfsstat_sysctl as nfsstat_sysctl
import isi.nfs.nfsstat.constants as const
from isi.nfs.nfsstat.formatter import *
import isi.nfs.op_codes as op_codes
import isi.nfs.rpc_client as rpc_client
from isi.nfs.nfsstat.server.stats import get_server_stats
from isi.nfs.rpc_client import NFSMGMT_STAT

def atLeastTwo(a, b, c):
    return (a and(b or c)) or (b and c)

def parse_opnum(opts, parser, text):
    """Turn a string containing either the name or opnum of an nfs
    operation into the operation number"""
    v3_opnum = v4_opnum = None

    try:
        v3_opnum = op_codes.str_to_opnum('nfs3', text)
    except ValueError:
        if opts.nfs3_only:
            parser.error("NFS v3 has no such operation '%s'" %text)

    try:
        v4_opnum = op_codes.str_to_opnum('nfs4', text)
    except ValueError:
        if opts.nfs4_only:
            parser.error("NFS v4 has no such operation '%s'" %text)

    if v3_opnum is None and v4_opnum is None:
        parser.error("Invalid NFS operation '%s'" %text)

    return v3_opnum, v4_opnum

def parse_histogram_opts(opts, parser):
    if opts.histogram is not None:
        if opts.server_only or opts.client_only or opts.global_only:
            parser.error("Incompatible arguments: --histogram and "
                    "[--server-only|--client-only|--global-only]")

    opts.v3_opnum, opts.v4_opnum = parse_opnum(opts, parser, opts.histogram)

    return opts

def parse_interval_opts(opts, args, parser):
    opnums = []
    for op in args:
        opnums.append(parse_opnum(opts, parser, op))

    opts.percent = False

    return opnums

def get_arguments():
    parser = optparse.OptionParser(description="Display NFS statistics")

    parser.add_option('-3', '--nfs3-only', action='store_true',
            help='Show only NFSv3 statistics')
    parser.add_option('-4', '--nfs4-only', action='store_true',
            help='Show only NFSv4 statistics')
    parser.add_option('-g', '--global-only', action='store_true',
            help='Show only global NFS server statistics')
    parser.add_option('-c', '--client-only', action='store_true',
            help='Show only client statistics')
    parser.add_option('-s', '--server-only', action='store_true',
            help='Show only server statistics')
    parser.add_option('-r', '--reset', action='store_true', default=False,
            help='Reset counters')
    parser.add_option('-u', '--human-readable', action='store_true', default=False,
            help="Don't show human-readable values")
    parser.add_option('-p', '--percent', action='store_true', default=False,
            help="Don't show percentages")
    parser.add_option('-w', '--interval', action='store', type='float',
            metavar='TIME [OP ...]',
            help='Show a brief summary of activity every TIME seconds')
    parser.add_option('-e', '--errors', action='store', type='string',
            default=None, metavar='OP',
            help='Instead of basic statistics, show a table of error counts '
            'for the given operation')
    parser.add_option('-k', '--cache', action='store_true', default=False,
            help='Instead of basic statistics, show statistics for the '
            'caches maintained by the NFS server')

    """
    parser.add_option('-t', '--time-hist', action='store', type='string',
            default=None, metavar='OP',
            help='Instead of basic statistics, show a server response-'
            'time histogram for the given operation.')
    """

    parser.add_option('-d', '--debug', action='store_true', default=False,
            help=optparse.SUPPRESS_HELP)
    parser.add_option('--check-cluster', action='store_true',
            help=optparse.SUPPRESS_HELP)

    (opts, args) = parser.parse_args()

    # XXX TEMP - remove when time histograms go in
    opts.time_hist = None

    if opts.global_only:
        opts.server_only = True

    if opts.server_only and opts.client_only:
        parser.error("Incompatible arguments: --server-only, --client-only")

    if opts.client_only and (opts.errors or opts.time_hist or opts.cache):
        parser.error("Incompatible arguments: server statistics and --client-only")

    if atLeastTwo(opts.errors, opts.time_hist, opts.cache):
        parser.error("Incompatible arguments: --cache, --error-hist, --time-hist")

    if atLeastTwo(opts.global_only, opts.nfs3_only, opts.nfs4_only):
        parser.error("Incompatible arguments: --nfs3-only, --nfs4-only, --global-only")

    if opts.interval:
        args = parse_interval_opts(opts, args, parser)

    opts.show_server = not opts.client_only
    opts.show_client = not (opts.server_only or opts.errors or opts.time_hist)
    opts.show_globals = not (opts.nfs3_only or opts.nfs4_only)
    opts.show_v3 = not (opts.nfs4_only or opts.global_only)
    opts.show_v4 = not (opts.nfs3_only or opts.global_only)

    if opts.errors:
        opts.errors = parse_opnum(opts, parser, opts.errors)

    if opts.time_hist:
        opts.time_hist = parse_opnum(opts, parser, opts.time_hist)

    return (opts, args)

def build_formatter(opts):
    if opts.check_cluster:
        formatter = CheckClusterFormatter()
    else:
        formatter = TerminalFormatter(percent=opts.percent, col_width=15,
                human_readable=opts.human_readable)
    return formatter

def nfsstat_interval(opts, opnums):
    try:
        if len(opnums) == 0:
            # Build the default list of operations
            headers = ['Getattr', 'Lookup', 'Readlink', 'Read',
                    'Write', 'Rename', 'Access', 'Readdir']
            for name in headers:
                v3_opnum = v4_opnum = None
                if name in op_codes.V3_SHORT_NAMES:
                    v3_opnum = op_codes.V3_SHORT_NAMES.index(name)
                if name in op_codes.V4_SHORT_NAMES:
                    v4_opnum = op_codes.V4_SHORT_NAMES.index(name)
                opnums.append((v3_opnum, v4_opnum))

        else:
            headers = []
            for op in opnums:
                if op[0] is not None and op[0] < len(op_codes.V3_SHORT_NAMES):
                    headers.append(op_codes.V3_SHORT_NAMES[op[0]])

                elif op[1] is not None and op[1] < len(op_codes.V4_SHORT_NAMES):
                    headers.append(op_codes.V4_SHORT_NAMES[op[1]])

                else:
                    raise ValueError('Got bad opnum tuple:', op)

        formatter = IntervalFormatter(
                headers = headers,
                col_width = 8,
                percent = opts.percent,
                hr = opts.human_readable)

        server_conn = None
        if opts.show_server:
            logging.debug("About to contact server")
            server_conn = rpc_client.NfsMgmtRpcClient()

        client_conn = None
        if opts.show_client:
            version_list = []
            if opts.show_v3: version_list.append(3)
            if opts.show_v4: version_list.append(4)
            logging.debug("About to query client for versions %s", version_list)
            try:
                client_conn = nfsstat_sysctl.NfsSysctl(version_list)
            except nfsstat_sysctl.NoNfsException:
                # The NFS client is not enabled, causing the nfsstat sysctl to fail.
                # If they're only looking for the client, this is an error.
                # Otherwise, we just don't show client stats (server doesn't
                # use the sysctl).
                if opts.client_only:
                    logging.error('NFS client not enabled')
                    return 1
                client_conn = None

        old_data = None
        while True: # Break on Ctrl+C
            data = {}
            differences = {}
            if server_conn:
                def extract_opcounts(op_index, opcounts):
                    row = []
                    for op in opnums:
                        if op[op_index] is not None:
                            row.append(opcounts[op[op_index]])
                        else:
                            row.append('')
                    return row

                server_stats = get_server_stats(server_conn,
                        stat_type=NFSMGMT_STAT.BASIC, reset=False)

                if opts.show_v3:
                    data['Srv v3'] = extract_opcounts(0, server_stats.v3_procs)

                if opts.show_v4:
                    data['Srv v4'] = extract_opcounts(1, server_stats.v4_procs)

            if client_conn:
                client_stats = client_conn.get_client_stats()
                data['Client'] = []
                for op in opnums:
                    if (op[1] < len(const.EXT_NFSSTAT_OPNAMES) and
                            op_codes.V4_SHORT_NAMES[op[1]] == const.EXT_NFSSTAT_OPNAMES[op[1]]):
                        data['Client'].append(client_stats.rpccnt[op[1]])
                    else:
                        data['Client'].append('')

            if old_data:
                for key in data:
                    differences[key] = []
                    for i in range(0, len(data[key])):
                        differences[key].append(max((data[key][i] - old_data[key][i]), 0))

                formatter.display(differences)

            old_data = data
            time.sleep(opts.interval)

    except KeyboardInterrupt:
        pass

    return 0

def nfsstat_errors(opts):
    server_conn = None
    if opts.show_server:
        logging.debug("About to contact server")
        server_conn = rpc_client.NfsMgmtRpcClient()

    formatter = build_formatter(opts)

    if opts.show_v3 and opts.errors[0]:
        v3_errors = get_server_stats(server_conn,
                stat_type=NFSMGMT_STAT.V3_ERRORS, reset=opts.reset)
        v3_errors.set_formatter(formatter)
        v3_errors.print_all(opts.errors[0])

    if opts.show_v4 and opts.errors[1]:
        v4_errors = get_server_stats(server_conn,
                stat_type=NFSMGMT_STAT.V4_ERRORS, reset=opts.reset)
        v4_errors.set_formatter(formatter)
        v4_errors.print_all(opts.errors[1])

    return 0

def nfsstat_time_hist(opts):
    server_conn = None
    if opts.show_server:
        logging.debug("About to contact server")
        server_conn = rpc_client.NfsMgmtRpcClient()

    formatter = build_formatter(opts)
    time_histogram = get_server_stats(server_conn,
            stat_type=NFSMGMT_STAT.OPTIME, reset=opts.reset)
    time_histogram.set_formatter(formatter)
    time_histogram.print_all(opts.time_hist[0], opts.time_hist[1])

    return 0

def nfsstat_cache(opts):
    opts.percent = False
    server_conn = None
    if opts.show_server:
        logging.debug("About to contact server")
        server_conn = rpc_client.NfsMgmtRpcClient()

    formatter = build_formatter(opts)
    cache_stats = get_server_stats(server_conn,
            stat_type=NFSMGMT_STAT.CACHE, reset=opts.reset)
    cache_stats.set_formatter(formatter)
    cache_stats.print_all()

def nfsstat_basic(opts):
    # Get server info
    server_conn = None
    if opts.show_server: # or opts.histogram:
        logging.debug("About to contact server")
        server_conn = rpc_client.NfsMgmtRpcClient()

    # Get client info
    client_conn = None
    if opts.show_client:
        version_list = []
        if opts.show_v3: version_list.append(3)
        if opts.show_v4: version_list.append(4)
        logging.debug("About to query client for versions %s", version_list)
        try:
            client_conn = nfsstat_sysctl.NfsSysctl(version_list)
        except nfsstat_sysctl.NoNfsException:
            # The NFS client is not enabled, causing the nfsstat sysctl to fail.
            # If they're only looking for the client, this is an error.
            # Otherwise, we just don't show client stats (server doesn't
            # use the sysctl).
            if opts.client_only:
                logging.error('NFS client not enabled')
                return 1
            opts.show_client = False

    formatter = build_formatter(opts)

    # Do queries
    if opts.show_server:
        logging.debug("Calling get_stats with reset as %s", opts.reset)
        server_stats = get_server_stats(server_conn,
                stat_type=NFSMGMT_STAT.BASIC, reset=opts.reset)
        server_stats.set_formatter(formatter)
        logging.debug("Finished collecting server")

    if opts.show_client:
        client_stats = client_conn.get_client_stats(reset=opts.reset)
        client_stats.set_formatter(formatter)
        logging.debug("Finished querying client")

    # Print stats
    if opts.show_client:
        client_stats.print_all()
        formatter.newline()

    if opts.show_server:
        if opts.show_globals:
            server_stats.print_globals()
            formatter.newline()

            # Display some legacy info (for check_cluster.pl)
            if opts.check_cluster:
                formatter.display(["Server: # of current clients:"],
                        [(server_stats.v3_counters.clients +
                            server_stats.v4_counters.clients)])
                formatter.display(["Server: Running total of client connections:"],
                        [(server_stats.v3_counters.totalclients +
                            server_stats.v4_counters.totalclients)])
                formatter.display(["Server: # of clients waiting to be cleaned up:"],
                        [0])
                formatter.newline()

        if opts.show_v3:
            server_stats.print_v3()
            if opts.show_v4:
                formatter.newline()
        if opts.show_v4:
            server_stats.print_v4()

    return 0

def main():
    (opts, args) = get_arguments()
    if opts.debug:
        logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG)
    else:
        logging.basicConfig(format='%(levelname)s:%(message)s')

    try:
        if opts.interval:
            return nfsstat_interval(opts, args)

        elif opts.errors:
            return nfsstat_errors(opts)

        elif opts.time_hist:
            return nfsstat_time_hist(opts)

        elif opts.cache:
            return nfsstat_cache(opts)

        else:
            return nfsstat_basic(opts)

    except KeyboardInterrupt:
        print "Interrupted"
        return 130

    except Exception as e:
        # TODO: handle exceptions in other threads
        err_msg = str(e) or repr(e)
        sys.stderr.write(err_msg + "\n")

        if opts.debug:
            raise

        return 1

    return 0

if __name__ == '__main__':
    sys.exit(main())
