#!/usr/bin/env python

# Copyright C 2010 by Isilon Systems, Inc. All rights not expressly
# granted herein are reserved.
# Created in the United States of America.
#
# This digital media is protected by U.S. and international copyright and
# intellectual property laws. Unless otherwise specified, all information
# and screens appearing as part of this digital medium, including
# software, services, documents, text, images, icons, and logos design;
# the selection, assembly, arrangement, and design thereof; and the code
# that enables its presentation, are the sole property of Isilon Systems,
# Inc.
#
# Except as otherwise required by applicable law, no part of any
# copyrighted material may be reproduced, modified, translated,
# distributed, published, or transmitted by any means, electronic or
# mechanical, for any purpose, without the prior written consent of Isilon
# Systems.
#
# Isilon and Isilon Systems are registered trademarks of Isilon Systems,
# Inc.  Legion is a trademark of Isilon Systems, Inc.  All other
# trademarks and registered trademarks are the property of their
# respective holders.

import sys
import os
import string
import copy
from isi.config import ntp
import isi.app.lib.cluster as cluster

default_num_chimers = 3

keywords = ['key', 'prefer', 'burst', 'iburst', 'minpoll', 'maxpoll', 'ttl', \
            'version']

progname = os.path.basename(sys.argv[0])

USAGE_STR = r'''
Usage: ''' + progname + r''' COMMAND [ARGUMENTS ...]

Commands:

    help
      Print this help and exit.

    list
      List all configured info.

    add server SERVER [OPTION]
      Add SERVER to ntp.conf.  If ntp.conf is already
      configured for SERVER, the configuration will be replaced.
      You can specify any server option. See NTP.CONF(5)

    delete server SERVER
      Remove server configuration for SERVER if it exists.

    add exclude NODE [NODE...]
      Add NODE (or space separated nodes) to NTP excluded entry.
      Excluded nodes are not used for NTP communication with external
      NTP servers.

    delete exclude NODE [NODE...]
      Delete NODE (or space separated Nodes) from NTP excluded entry.

    keyfile KEYFILE_PATH
      Specify keyfile path for NTP auth. Specify "" to clear value.
      KEYFILE_PATH has to be a path under /ifs.

    chimers [COUNT | "default"]
      Display or modify the number of chimers NTP uses.
      Specify "default" to clear the value.
'''

class Usage(Exception):
    pass

def usage():
    print >> sys.stderr, USAGE_STR
    raise Usage

def print_chimers():
    chimers = ntp.get_num_chimers()
    if chimers != default_num_chimers:
        print "Chimers: %d" % (chimers)
    else:
        print "Chimers has not been modified from the default (%d)" % (default_num_chimers)
    print

def modify_chimers(args):
    chimer_num = int(args[0])
    if len(args) > 1 or chimer_num < 1:
        raise RuntimeError, 'Please specify one real number for Chimers.'
    chimers = ntp.set_num_chimers(chimer_num)
    print_chimers()

def set_default_num_chimers():
    chimers = ntp.set_num_chimers(default_num_chimers)
    print "NTP will use the default number of chimers (%d)" %(default_num_chimers)
    print

def list_servers():
    print 'NTP Server Setting:'
    servers = ntp.get_servers()
    if servers is None or len(servers) == 0:
        print 'No NTP Server is configured.'
    else:
        for server in servers:
            print server

def list_exclude_lnn():
    excluded = ntp.get_excluded()
    true_excluded = []
    if excluded:
        for i in excluded:
            if not i or i.isspace():
                print "Empty excluded entry found (ignored)."
            else:
                try:
                    true_excluded.append(int(i))
                except ValueError:
                    print "Invalid excluded entry found:", i, "(ignored)"
                    
    print "Nodes excluded from contacting external NTP servers:"
    if len(true_excluded) == 0:
        print "No nodes are excluded from contacting external NTP servers."

    else:
        true_excluded.sort()
        for i in true_excluded:
            print "%d " % i
        print

def list_config():
    print 'NTP Auth Keyfile:'
    keyfile = ntp.get_key_file()
    if not keyfile:
        print 'No Keyfile is configured.'
    else:
        print keyfile

def add_server(args):
    if len(args) == 0:
        raise RuntimeError, "Please specify server to add"
    for i in args[1:]:
        if i.isdigit():
            if not (0 < int(i) < 65535):
                raise RuntimeError, 'The keyword is not valid.'
        else:
            if not i in keywords:
                raise RuntimeError, 'The keyword is not valid.'

    new_server = " ".join(args)
    servers = ntp.get_servers()
    if servers:
        for server in servers:
            if server.split(" ")[0] == args[0]:
                servers.remove(server)
        servers.append(new_server)
    else:
        servers = [new_server]
    ntp.set_servers(servers)

    print args[0], 'is added.'
    list_servers()

def delete_server(args):
    deleted = False
    if len(args) != 1:
        raise RuntimeError, 'Delete command requires one argument'
    servers = ntp.get_servers()
    if servers:
        for server in servers:
            if server.split(" ")[0] == args[0]:
                servers.remove(server)
                deleted = True
    if deleted:
        ntp.set_servers(servers)
        print args[0], 'is deleted.'
    else:
        print '%s is not currently configured as NTP server.' % args[0]
    list_servers()

def config_keyfile(args):
    if len(args) != 1:
        raise RuntimeError, 'Please specify one keyfile.'

    try:
        if args[0] != "" and args[0].index('/ifs/') != 0:
            raise RuntimeError, 'Please specify a valid path under /ifs'
    except ValueError:
        raise RuntimeError, 'Please specify a valid path under /ifs'

    ntp.set_key_file(args[0])
    list_config()

def add_exclude_lnn(args):
    if len(args) == 0:
        raise RuntimeError, "Please specify node number to add"

    up_nodes_by_lnn = map(lambda n:n.lnn(), cluster.Cluster().online())
    up_nodes_by_lnn.sort()

    arg_lnns = []
    for lnn in args:
        try:
            arg_lnns.append(int(lnn))
            if not arg_lnns[-1] in up_nodes_by_lnn:
                extra = ""
                if 1 <= arg_lnns[-1] <= up_nodes_by_lnn[-1]:
                    extra = ", but no node %s in the range." % lnn
                err = "Invalid node number %s. Node range is %d to %d%s" % \
                      (lnn, up_nodes_by_lnn[0], up_nodes_by_lnn[-1], extra)
                raise RuntimeError, err
        except ValueError:
            err =  "Invalid node number:", lnn
            raise RuntimeError, err

    mod_exclude = ntp.get_excluded()

    if not mod_exclude:
        mod_exclude = []

    for lnn in arg_lnns:
        if str(lnn) in mod_exclude:
            print "Node", lnn, "is already excluded (ignored)."
        else:
            mod_exclude.append(str(lnn))
    ntp.set_excluded(mod_exclude)
    list_exclude_lnn()

def delete_exclude_lnn(args):
    if len(args) == 0:
        raise RuntimeError, "Please specify node's lnn to add"

    up_nodes_by_lnn = map(lambda n:n.lnn(), cluster.Cluster().online())
    up_nodes_by_lnn.sort()

    arg_lnns = []
    for lnn in args:
        try:
            arg_lnns.append(lnn)
            if not arg_lnns[-1] in up_nodes_by_lnn:
                extra = ""
                if 1 <= arg_lnns[-1] <= up_nodes_by_lnn[-1]:
                    extra = ", but no node %s in the range." % lnn
                    err = "Invalid node number %s. Node range is %d to %d%s" % \
                      (lnn, up_nodes_by_lnn[0], up_nodes_by_lnn[-1], extra)
                    raise RuntimeError, err
        except ValueError:
            err =  "Invalid node number:", lnn
            raise RuntimeError, err

    mod_exclude = ntp.get_excluded()
    if not mod_exclude:
        mod_exclude = []
    current_lnns = set(mod_exclude)

    # Mark lnns to remove as False
    some_to_delete = False
    for i in arg_lnns:
        if i in current_lnns:
            mod_exclude.remove(i)
            current_lnns.remove(i)
            some_to_delete = True
        else:
            print "Node", i, "is not currently excluded (ignored)."

    if some_to_delete:
        ntp.set_excluded(mod_exclude)

        list_exclude_lnn()

def main():
    if len(sys.argv) < 2:
        usage()

    verb = sys.argv[1]
    args = sys.argv[2:]

    if verb == 'help':
        usage()
    elif verb == 'list':
        if len(args) == 0:
            list_servers()
            print ''
            list_exclude_lnn()
            print_chimers()
            list_config()
        elif args[0] == 'server':
            list_servers()
        elif args[0] == 'exclude':
            list_exclude_lnn()
        else:
            raise RuntimeError, 'Invalid args.'
    elif verb == 'add':
        if len(args) == 0:
            usage()
        if args[0] == 'server':
            args = args[1:]
            add_server(args)
        elif args[0] == 'exclude':
            args = args[1:]
            add_exclude_lnn(args)
        else:
            raise RuntimeError, 'Add command takes subcommand'
    elif verb == 'delete':
        if len(args) == 0:
            usage()
        if args[0] == 'server':
            args = args[1:]
            delete_server(args)
        elif args[0] == 'exclude':
            args = args[1:]
            delete_exclude_lnn(args)
        else:
            raise RuntimeError, 'Delete command takes subcommand'
    elif verb == 'keyfile':
        config_keyfile(args)
    elif verb == 'chimers':
        if len(args) == 0:
            print_chimers()
        elif args[0].isdigit():
            modify_chimers(args)
        elif args[0] == "default":
            set_default_num_chimers()
        else:
            sys.stderr.write("Chimers only accept one real number.\n\n")
            usage()
    else:
        raise RuntimeError, "Unknown command '%s'" % verb

def error(str):
    print >> sys.stderr, progname + ':', str
    print >> sys.stderr, "Use '%s help' for more information" % progname
    sys.exit(1)

if __name__ == '__main__':
    try:
        main()
    except Usage:
        pass
    except Exception, e:
        error('%s' % e)
