#!/usr/bin/python
import json
import logging
import optparse
import socket
import string
import sys
import traceback

import isi.nfs.rpc_client as rpc_client
import isi.fs.lin

def confirm(prompt):
    """ Print a message to the command line, and ask for confirmation
    from the user """
    while True:
        answer = raw_input(prompt + "? (yes/[no]): ").upper()
        if len(answer) > 0 and len(answer) <= 3:
            if answer == 'YES' or answer == 'Y':
                return True
            if answer == 'NO' or answer == 'N':
                return False
        else:
            print "Not confirmed"
            return False

def format_hex(hex_str):
    """ value should be a string of hex characters, such
    as '0x1234' or '0xdeadbeef'.  Returns two nicely formatted
    versions (ascii and hex) of the input """

    if hex_str.startswith('0x'):
        hex_str = hex_str[2:]

    formatted_hex = "(hex): 0x" + hex_str

    ascii = "(ascii): "
    for byte in bytearray(hex_str.decode('hex')):
        if chr(byte) in string.printable and chr(byte) not in string.whitespace:
            ascii += chr(byte)
        else:
            ascii += '.'
    return formatted_hex, ascii

def lin_to_path(lin, snap):
    try:
        return '/ifs/' + isi.fs.lin.getPath((lin, snap))
    except IOError:
        return '<Could not translate LIN>'

def format_lin(lin):
    return hex(long(lin)).rstrip('L')

def print_client(client_dump, print_names):
    """ Dump important information about an NFSv4 client to
    the command line in a human-readable format """
    print "Dump of client", client_dump['Table Index']
    open_count = 0
    open_handle_count = 0
    lock_owner_count = 0
    print "  Open Owners (%s):" % len(client_dump['Open Owners'])
    for open_owner in client_dump['Open Owners']:
        oo_hex, oo_ascii = format_hex(open_owner['Name'])
        print "    Name", oo_hex
        print "    Name", oo_ascii
        print "      Opens (%s):" % len(open_owner['Opens'])
        open_count += len(open_owner['Opens'])
        for open in open_owner['Opens']:
            print "        Open Index:", open['Table Index']
            if print_names:
                print "          File Name:", lin_to_path(open['LIN'], open['Snap'])
            print "          File LIN:", format_lin(open['LIN'])
            print "          Snap ID:", open['Snap']
            print "          State:", open['State']
            print "          Handles (%s):" % len(open['Open Handles'])
            open_handle_count += len(open['Open Handles'])
            for open_handle in open['Open Handles']:
                print "            ", open_handle['FileHandle']
            print "          Lock Owners (%s):" % len(open['Lock Owners'])
            lock_owner_count += len(open['Lock Owners'])
            for lock_owner in open['Lock Owners']:
                print "            Lock Index:", lock_owner['Table Index']
                print "              State:", lock_owner['State']
                # XXX DPR We don't print the Owner ID here -
                #   is it important that users be able to see it?

    print
    print "Summary of Client %s:" % client_dump['Table Index']
    ln_hex, ln_ascii = format_hex(client_dump['Long ID'])
    print "  Long Name", ln_hex
    print "  Long Name", ln_ascii
    print "  Callback Address:", client_dump['Callback Address']
    print "  Connection:", client_dump['Callback NetID']
    print "  State:", client_dump['State']
    print "  Open Owners:", len(client_dump['Open Owners'])
    print "  Opens:", open_count
    print "  Open Handles:", open_handle_count
    print "  Lock Owners:", lock_owner_count

class Nfs4Mgmt:
    def __init__(self):
        self.debug = False
        self.opts = None
        self.args = None
        self._nfsmgmt_client = None
        self.privileged = False

    def get_nfsmgmt_client(self, privileged=False):
        if privileged and not self.privileged and self._nfsmgmt_client:
            self._nfsmgmt_client.close_connection()
            self._nfsmgmt_client = None

        if self._nfsmgmt_client is None:
            self._nfsmgmt_client = rpc_client.NfsMgmtRpcClient(
                    privileged=privileged, use_tcp=True)

        return self._nfsmgmt_client

    def build_parser(self):
        parser = optparse.OptionParser(
                description="Display and modify NFSv4 server state")

        parser.add_option('-d', '--debug', action='store_true', default=False,
                help=optparse.SUPPRESS_HELP)
        parser.add_option('-f', '--force', action='store_true', default=False,
                help='Never ask for confirmation on dangerous actions')

        views = optparse.OptionGroup(parser, "Display options",
                "Options to display NFSv4 server state")
        views.add_option('--list', action='store_true',
                help='List clients, displaying a short summary of each')
        views.add_option('--dump', action='store', type='string',
                metavar='CLIENT', help='Display all information on a client')
        views.add_option('--load-dump', action='store', type='string',
                metavar='FILE.dump', help='Load a client dump from disk')
        views.add_option('-j', '--json', action='store_true', default=False,
                help="Spit the raw JSON from a dump request directly to stdout")
        views.add_option('-n', '--no-names', action='store_false', default=True,
                dest='print_names',
                help="Don't translate LINs to file names for --dump")
        views.add_option('-o', '--out-file', action='store', type='string',
                metavar='PATH', help='Where to save full dump results')
        parser.add_option_group(views)

        actions = optparse.OptionGroup(parser, "Modification options",
                ("Options to modify NFSv4 server state. Please note that these "
                    "options are EXTREMELY DANGEROUS and should only be invoked "
                    "by trained professionals in extenuating circumstances!"))
        actions.add_option('--revoke-open', action='store', nargs=2,
                type='long', metavar='CLIENT_ID OPEN_INDEX',
                help='Revoke an open and destroy all associated state')
        actions.add_option('--revoke-lock', action='store', nargs=2,
                type='long', metavar='OPEN_INDEX LOCK_INDEX',
                help='Revoke a single lock and destroy all associated state')
        actions.add_option('--destroy', action='store', type='long',
                metavar='CLIENT_ID',
                help='Destroy all state associated with a client')
        parser.add_option_group(actions)
        return parser

    def resolve_client(self, parser, client_string):
        try:
            client_id = long(client_string)
            if client_id < 1:
                parser.error("Client ids must be positive integers")
            return client_id

        except ValueError:
            try:
                addr = socket.gethostbyname(client_string)
                clients = self.get_clients_by_ip(addr)
                if len(clients) < 1:
                    # Try the raw string provided by the user
                    clients = self.get_clients_by_ip(client_string)

                if len(clients) < 1:
                    parser.error('No client has "%s" registered as a callback '
                            'address' % client_string)
                elif len(clients) > 1:
                    parser.error('Multiple clients have "%s" registered as a '
                            'callback address' % client_string)
                else:
                    return long(clients[0]['ClientID'])

            except socket.error:
                parser.error('Argument to --dump should be a callback '
                        'address or client id (got "%s")' % client_string)

    def get_arguments(self):
        parser = self.build_parser()
        opts, self.args = parser.parse_args()

        self.debug = opts.debug

        action_count = 0
        if opts.list:
            action_count += 1
        if opts.dump:
            action_count += 1
        if opts.load_dump:
            action_count += 1
        if opts.revoke_open:
            action_count += 1
        if opts.revoke_lock:
            action_count += 1
        if opts.destroy:
            action_count += 1

        if action_count > 1:
            parser.error("Only one operation at a time, please")

        elif action_count == 0:
            opts.list = True

        if opts.dump:
            opts.client_id = self.resolve_client(parser, opts.dump)

        if opts.json and opts.dump is None:
            parser.error("--json only applies to client dumps")

        if opts.revoke_open or opts.revoke_lock or opts.destroy:
            opts.privileged = True
        else:
            opts.privileged = False

        self.opts = opts

    def get_clients_by_ip(self, client_addr):
        nfsmgmt_client = self.get_nfsmgmt_client()
        client_list = nfsmgmt_client.get_clients()
        matches = []
        for client in client_list:
            if client_addr in client['Address']:
                matches.append(client)

        return matches

    def list_clients(self, addr=None):
        if addr:
            client_list = self.get_clients_by_ip(addr)
        else:
            nfsmgmt_client = self.get_nfsmgmt_client()
            client_list = nfsmgmt_client.get_clients()

        sorted_clients = sorted(client_list, key=lambda k: k['ClientID'])
        print "%s%s%s%s%s%s%s" % (
                "ID".ljust(9),
                "Conn".ljust(9),
                "Callback Address".ljust(26),
                "O-Owners".ljust(9),
                "Opens".ljust(9),
                "Handles".ljust(9),
                "L-Owners".ljust(9),)
        for client in sorted_clients:
            print "%s%s%s%s%s%s%s" % (
                    str(client['ClientID']).ljust(9),
                    str(client['Connection']).ljust(9),
                    str(client['Address']).ljust(26),
                    str(client['Open Owners']).ljust(9),
                    str(client['Opens']).ljust(9),
                    str(client['Open Handles']).ljust(9),
                    str(client['Lock Owners']).ljust(9),)

    def dump_client(self, clientid):
        nfsmgmt_client = self.get_nfsmgmt_client()
        client_dump = nfsmgmt_client.dump_client(clientid, path=self.opts.out_file)
        if self.opts.json:
            json.dump(client_dump, sys.stdout)
        else:
            print_client(client_dump, self.opts.print_names)
            if self.opts.out_file is None:
                path = rpc_client.DUMP_CLIENT_PATH
            else:
                path = self.opts.out_file
            print "Full JSON dump can be found at", path

    def load_dump(self, filename):
        try:
            json_file = open(filename, 'r')
            print_client(json.load(json_file), self.opts.print_names)
        except IOError:
            sys.stderr.write('Could not open client dump at %s' % filename)
            sys.exit(1)


    def destroy_client(self, clientid):
        if not self.opts.force and not confirm(
                "Are you sure you want to destroy all state "
                "associated with client %s" % clientid):
            return

        nfsmgmt_client = self.get_nfsmgmt_client(True)
        nfsmgmt_client.destroy_client(clientid)

    def revoke_open(self, clientid, open_index):
        if not self.opts.force and not confirm(
                "Are you sure you want to revoke all state associated with open "
                "%s under client %s" % (open_index, clientid)):
            return

        nfsmgmt_client = self.get_nfsmgmt_client(True)
        nfsmgmt_client.revoke_open(clientid, open_index)

    def revoke_lock(self, open_index, lock_index):
        if not self.opts.force and not confirm(
                "Are you sure you want to revoke all state associated with lock "
                "%s under open %s" % (lock_index, open_index)):
            return

        nfsmgmt_client = self.get_nfsmgmt_client(True)
        nfsmgmt_client.revoke_lock(open_index, lock_index)

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

        if self.opts.list:
            self.list_clients()

        elif self.opts.dump:
            self.dump_client(self.opts.client_id)

        elif self.opts.load_dump:
            self.load_dump(self.opts.load_dump)

        elif self.opts.destroy:
            self.destroy_client(self.opts.destroy)

        elif self.opts.revoke_open:
            self.revoke_open(self.opts.revoke_open[0], self.opts.revoke_open[1])

        elif self.opts.revoke_lock:
            self.revoke_lock(self.opts.revoke_lock[0], self.opts.revoke_lock[1])


def main():
    exit_code = 0

    try:
        nfs4mgmt = Nfs4Mgmt()
        nfs4mgmt.run()

    except KeyboardInterrupt:
        print "Interrupted"
        exit_code = 130

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

        exit_code = 1

    sys.exit(exit_code)

if __name__ == '__main__':
    main()
