#!/usr/bin/python2.4

"""
Detect duplicate servers discovered via SP, OS, or manually

"""

import sys, os
import re
import logging
import gettext
import locale
import getopt
import codecs
import time
import xml.dom.minidom
from datetime import datetime, timedelta

# Adjust search path to include rest of n1sh files.
sys.path.append(os.path.join(sys.path[0], 'cli'))
import executor
import n1shmain

ALLOWED_LANGUAGES = ('en',)  # This script understands only english.

RE_JOB_STARTED = re.compile(r'Job "(\d+)" started.')

JOB_DONE_SUCCESS = ('Completed',)
JOB_DONE_ERROR = ('Warning', 'Error', 'Stopped', 'Timed Out',)
JOB_RUNNING = ('Running','Not Started')
JOB_TYPE_DISCOVERY = ('Discovery',)

PAUSE_SEC = 5


def main((cout, cerr), argv):
    pargs = parse_args(cerr, argv)
    if not pargs:
        usage(cerr)
        sys.exit(-1)

    servernames, loglevel, role = pargs

    # curr_handler will need to be removed later when the encoded stream
    # is determined
    curr_handler = init_logger(cerr, loglevel)

    terminal_encoding = init_locale()
    session = n1shmain.Session(role, 'xml', terminal_encoding)

    writer = codecs.getwriter(session.encoding)
    cout = writer(cout, 'replace')
    cerr = writer(cerr, 'replace')

    # Reset handler to encoded cerr.
    init_logger(cerr, loglevel, curr_handler)

    logging.info('stdin, stdout and stderr set to "%s" encoding.'
                 % session.encoding)
    logging.info('servers=%s; role="%s"'
                  % (servernames, role))

    status = 0
    cacao = executor.CacaoExecutor()
    errors = []
    successes = []

    try:
        try:
            cacao.start(session)
            allservers = []
            servers = {}
            jobs = get_jobs(cacao)
            if (is_job_exist(jobs, JOB_TYPE_DISCOVERY, JOB_RUNNING)):
                print >>cerr, 'Cannot run detection while discovery jobs are running.'
                status = 1
                return status
                
            print >>cout, 'Searching for duplicate servers (Please wait) ...'
            # get all servers if not specified on command line
            if (not servernames):
                servernames = parse_all_servers(cacao.execute('show server'), errors)
            logging.info(servernames)

            # get server details
            for myServer in servernames:
                server = parse_result_server(cacao.execute('show server %s' % myServer), errors)
                if (server):
                    servers[server.name] = server

            # locate duplicates
            if not servers:
                print >>cout, 'No servers found.'
            else:
                serverMap = findDuplicateServers(servers)
                printDuplicates(servers, serverMap, cout)

        finally:
            status = cacao.stop()
    except IOError, e:
        errors.append('Error accessing N1SM: ' + str(e))

    for e in errors:
        print >>cerr, e

    logging.shutdown()
    return status

def printDuplicates(servers, serverMap, cout):
    # convert the union/find map into lists of duplicate servers 
    dups = {}
    for server, i in serverMap.iteritems():
        if (not dups.has_key(i)):
            dupList = []
            dups[i] = dupList
        dups[i].append(servers[server])

    logging.info('Dup map "%s"' % (dups,))
    print >>cout,'%-15s%-15s%-15s%-15s' % ('Name', 'Hardware', 'Discovered At', 'Network') 
    dupsFound = False
    for dups in dups.values():
        if (len(dups) > 1):
            dupsFound = True
            for i, server in enumerate(dups):
                type = getDiscoveryNetworkType(server)
                ip = server.ip
                output = '%-15s%-15s%-15s%-15s' % (server.name, server.model, ip, type)
                print >>cout, output
            print >>cout 
    if not dupsFound: print >>cout 

def findDuplicateServers(servers):
    #
    # This is based on a simple but not very efficient union/find algorithm as described in 
    # R. Sedgewick's Algorithms vol 1 to solve a general connectivity 
    # problem. It takes a list of all (server, mac) pairs
    # and computes which servers are 'connected' to each other
    # via common mac addresses. Connected servers are duplicates
    #
    macMap = {}
    serverMap = {}
    for server in servers.values():
        ports = server.ports
        for mac in ports.values():
            newMac = False
            newServer = False
                                                                                                       
            # e[x] = y iff e is connected to y
            if (not macMap.has_key(mac)):
                macMap[mac] = mac
                newMac = True
            if (not serverMap.has_key(server.name)):
                serverMap[server.name] = mac
                newServer = True
                                                                                                         
            # new pair, just initialize
            if (newMac and newServer):
                logging.info("initialized  %s %s" % (server.name, mac))
                continue;
           
            oldserver = serverMap[server.name]
            serverMap[server.name] = mac
            oldmac = macMap[mac]
            macMap[mac] = mac

            logging.info("unioning %s %s oldserver = %s, oldmac= %s" % (server.name, mac, oldserver, oldmac))
            # e[x] is connected to oldserver|oldmac => e[x] is connected to mac as well
            # update all entries, effectively taking the union

            for s, m in serverMap.iteritems():
                logging.info("testing %s : %s" % (s,m))
                if (m == oldserver or m == oldmac):
                    logging.info("changing %s => %s" % (s,mac))
                    serverMap[s] = mac

            for e, m in macMap.iteritems():
                logging.info("testing %s : %s" % (e,m))
                if (m == oldserver or m == oldmac):
                    logging.info("changing %s => %s" % (e,mac))
                    macMap[e] = mac

    # at the end of iteration, all servers which are duplicates will have the same value in serverMap[x]
    logging.info('union/find map "%s"' % (serverMap,))
    return serverMap

def getDiscoveryNetworkType(server):
    type = 'Management'
    if (server.powercontrol == 'Not Supported'):
        if (server.ip == '-'):
            type = 'File'
        else:
            type = 'Data'
    return type

def usage(strm):
    print >>strm, 'Find duplicate servers in N1SM\n'
    print >>strm, 'Usage:', os.path.basename(sys.argv[0]), ' [server [server ...]]'


def parse_args(cerr, argv):
    """
    Return (<list of servers>, arguments, loglevel, role) or None if
    there's a problem.
    """
    try:
        opts, args = getopt.gnu_getopt(argv, '',[ 'debug', 'role=',])
    except getopt.GetoptError, e:
        print >>cerr, e
        print >>cerr
        return

    loglevel = sys.maxint
    role = ''
    for o, v in opts:
        if o in ('--debug',):
            loglevel = logging.NOTSET
        elif o in ('--role',):
            role = v

    return args, loglevel, role


def init_logger(strm, lvl, old_handler=None):
    """Configure logger.  Remove the old handler, if there was one."""
    logger = logging.getLogger()
    handler = logging.StreamHandler(strm)
    formatter = logging.Formatter('[%(levelname)8s] %(message)s')
    handler.setFormatter(formatter)
    if old_handler:
        logger.removeHandler(old_handler)
    logger.addHandler(handler)
    logger.setLevel(lvl)

    return handler


def init_locale():
    """Make sure the message catalog for gettext is configured correctly."""
    # Make sure locale is set correctly.
    try:
        loc = locale.setlocale(locale.LC_ALL, '')
        try:
            lang, enc = loc.split('.')
        except ValueError:
            lang = loc
            enc = ''
        enc = enc.split('@')[0]
        if lang.split('_')[0] not in ALLOWED_LANGUAGES:
            # Set to a default locale if none set.  Let system determine
            # the default encoding.
            def_lang = 'en_US'
            #if enc:
            #    def_lang += '.' + enc

            logging.debug('Locale "%s" not in %s.  Using "%s".'
                         % (loc, str(ALLOWED_LANGUAGES), def_lang))
            locale.setlocale(locale.LC_ALL, def_lang)

    except locale.Error, e:
        logging.error('%s: %s' % (e.__class__, str(e)))
        print 'The current locale is not supported.'
        sys.exit(n1shmain.CODE_INTERNAL_ERROR)

    langvar = 'LANGUAGE'
    try:
        l = locale.getlocale(locale.LC_ALL)[0]
        language = os.environ[langvar].split('.')[0]
        logging.debug('%s = %s; should be "%s"' % (langvar, language, l))
        if language != l:
            raise locale.Error  # Borrow so we don't need our own exception.
    except (KeyError, locale.Error):
        # Use the current locale (which may be from default) to
        # set 'langvar' environment variable for the duration of this run.
        l = locale.getlocale(locale.LC_ALL)
        os.environ[langvar] = '%s' % (l[0])
        logging.debug('Setting default %s = "%s"'
                     % (langvar, os.environ[langvar]))


    # Note:  If using symlinks for n1sh.py, the "sys.path[0]" trick may not
    #        work.  Use the commented out line (hardcoded path) instead.
    gettext.bindtextdomain('n1sh', os.path.join(sys.path[0], 'cli/locale'))
    #gettext.bindtextdomain('n1sh', '/opt/sun/n1gc/bin/cli/locale')
    gettext.textdomain('n1sh')

    return enc


class JobExecuteError(Exception):
    """Job didn't start or finished unsuccessfully."""


# XML results
class Base(object):
    """Some default features."""
    def __str__(self):
        return self.__class__.__name__ + " " + str(vars(self))


class Job(Base):
    """Salient job data."""
    def __init__(self, jobid, status, type):
        self.id = jobid
        self.status = status
        self.type = type

    def __cmp__(self, o):
        return cmp(int(self.id), int(o.id))


class Server(Base):
    """Salient server data."""
    def __init__(self, name, power, osbasemgmt, osmonitor,
                 oshealth, hwhealth, monitor, ip, ports, powercontrol, model):
        self.name = name
        self.power = power
        self.osbasemgmt = osbasemgmt
        self.osmonitor = osmonitor
        self.oshealth = oshealth
        self.hwhealth = hwhealth
        self.hwmonitor = monitor
        self.ip = ip
        self.ports = ports
        self.powercontrol = powercontrol
        self.model = model

    def __cmp__(self, o):
        return cmp(self.name, o.name)



def parse_result_jobs(result):
    """Return map of {"id" => Job()} from output xml."""
    alljobs = {}

    #print result
    if result.status == 0:
        dom = xml.dom.minidom.parseString(result.output)
        for job in dom.getElementsByTagName('job'):
            jid = get_text_from_node(job, 'id')
            jstatus = get_text_from_node(job, 'job_status')
            jtype = get_text_from_node(job, 'type')
            alljobs[jid] = Job(jid, jstatus, jtype)

    return alljobs


def parse_all_servers(result, errors):
    servers = []
    logging.info(result.output)
    all = xml.dom.minidom.parseString(result.output.encode('utf-8'))
    for server in all.getElementsByTagName('server'):
        sname = get_text_from_node(server, 'name') 
        servers.append(sname)
    return servers

def parse_result_server(result, errors):
    """Return map of {"name" => Server()} from output xml."""

    logging.info(result)

    if result.status != 0:
        return ''

    if result.status == 0:
        server = xml.dom.minidom.parseString(result.output.encode('utf-8'))
        sname = get_text_from_node(server, 'name')
        spower = get_text_from_node(server, 'power')  # "On"
        sosbase = get_text_from_node(server, 'OsBasemgmtSupported') # "Yes"
        sosmonitor = get_text_from_node(server, 'OsMonitoringSupported') # "Yes"
        soshealth = get_text_from_node(server, 'OSHealth')   # "Good"
        shwhealth = get_text_from_node(server, 'hardwareHealth')   # "Good"
        smonitor = get_text_from_node(server, 'monitorstate') # "Enabled"
        capabilities = server.getElementsByTagName('capabilities').item(0)
        spowercontrol = get_text_from_node(capabilities, 'PowerWrite')
        smonitor = get_text_from_node(server, 'monitorstate') # "Enabled"
        smodel = get_text_from_node(server, 'hardware') # "Enabled"
        ip = get_text_from_node(server, 'ip');
        logging.info(server)  
        ports = {}
        for port in server.getElementsByTagName('port'):
            network = get_text_from_node(port, 'network')
            if (network == 'Data'):
                name = get_text_from_node(port, 'name')
                mac = get_text_from_node(port, 'mac')
                ports[name] = mac
      
        newServer = Server(sname, spower, sosbase,
                                   sosmonitor, soshealth, shwhealth,
                                   smonitor, ip, ports, spowercontrol, smodel)


    return newServer


# helpers
def get_text_from_node(dom, n):
    """Return the text value of the first node named "n"."""
    return get_text(dom.getElementsByTagName(n)[0].childNodes)


def get_text(nodelist):
    """Return all the gathered text from node."""
    rc = ""
    for node in nodelist:
        if node.nodeType == node.TEXT_NODE:
            rc = rc + node.data
    return rc


def is_job_exist(jobs, type, status):
    for job in jobs.values():
        if (job.status in status and job.type in type):
            return True
    return False


def get_job(cacao, id):
    """Return job with id "id"."""
    return get_jobs(cacao)[id]

def get_jobs(cacao):
    result = cacao.execute('show job')
    if result.status != 0:
        logging.warning(result.output)
        raise IOError('Unknown Error')
    return parse_result_jobs(result)


def wait_for_job(strm, cacao, id, now):
    """Return whether job completed successfully or failed."""
    strm.write('...waiting for job %s.' % (id))
    strm.flush()
    while True:
        job = get_job(cacao, id, now)
        if job.status in JOB_DONE_SUCCESS + JOB_DONE_ERROR:
            print >>strm
            return job.status

        # Wait a bit
        for s in range(1, PAUSE_SEC):
            time.sleep(1)
            strm.write('.')
            strm.flush()


def execute_job(strm, cacao, cmd):
    """
    Execute command and wait for job to finish.

    Raise JobExecuteError if job failed.
    """
    now = (datetime.utcnow() - timedelta(seconds=60)).strftime('%Y-%m-%dT%H:%M:%SZ')
    #print '[[[ "%s" ]]]' % now
    print >>strm, '        starting job',
    strm.flush()
    result = cacao.execute(cmd)
    if result.status == 0:
        jobid = RE_JOB_STARTED.search(result.output.strip()).group(1)
        jstate = wait_for_job(strm, cacao, jobid, now)
        if jstate not in JOB_DONE_SUCCESS:
            raise JobExecuteError('Job did not complete successfully; jobstate=%s' % jstate)
    else:
        logging.warning(result.output)
        raise JobExecuteError('Job could not start; %s' % cmd)


def quote(s):
    """Return quoted string s."""
    return u'"%s"' % s.replace('\\', '\\\\').replace('"', '\\"')



if __name__ == '__main__':
    status = main((sys.stdout, sys.stderr), sys.argv[1:])
    sys.exit(status)




