#!/usr/bin/env python
# -----------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
# -----------------------------------------------------------------------

#
# This script starts a simulated DUCC cluster.  All processes are real, so all
# codepaths are executed.  The simulation comes from running multiple DUCC agents
# on a single node, allowing large-scale testing on small-scale clusters.  All 
# the system-test jobs sleep rather than compute and as such are very small and
# inexpensive, which allows large-scale testing to be performed on minimal 
# hardware.
#

import os
import sys
import time
import getopt

from threading import *
import Queue

#designed to run only here, relative to ducc_runtime
DUCC_HOME = os.path.abspath(__file__ + '/../../..')
sys.path.append(DUCC_HOME + '/admin')

from ducc_util  import DuccUtil
from properties import Properties
from ducc       import Ducc
from ducc_util  import ThreadPool

# multi-threaded start can overwhelm ssh if going to the same target host.  We inject
# a short sleep between starts to make it better.  This is how long to sleep in secs.
# Note: when 0.1 the ducc.properties merge step run by each agent caused problems/hangs
global SLEEP_TIME
SLEEP_TIME = 0.5

class StartSim(DuccUtil):

    def __init__(self):
        DuccUtil.__init__(self, True)

    def start_broker(self):
        
        if ( not self.automanage_broker ):
            print "Broker is not automanaged, returning."
            return

        broker_host = self.localhost
        print 'broker host', broker_host
        lines = self.ssh(broker_host, True, "'", self.DUCC_HOME + '/admin/ducc.py', '-c', 'broker', '--simtest', "'")

        for i in range(0, 9):
            if ( self.is_amq_active() ):
                return
            print 'Waiting for broker', str(i)
            time.sleep(1)

    def start_database(self):
        
        if ( not self.automanage_database ):
            print "Database is not automanaged, returning."
            return
		
        try:
        	self.db_start()                
        except Exception (e):
            # print e
            print sys.exc_info()[0], "Can't start the database."
            sys.exit(1)
		
    def run_local_agent(self, pnode, ip, memory ):

        if ( not self.verify_jvm() ):
            return        
        self.verify_duccling()
        self.verify_limits()

        memory = int(memory) * 1024 * 1024    # to GB from KB
        CMDPARMS = []
        CMDPARMS.append(self.java())
        CMDPARMS.append('-Dducc.deploy.components=agent')
        CMDPARMS.append('-Dos.page.size=' + self.os_pagesize)
        CMDPARMS.append('-Dducc.deploy.configuration=' + self.DUCC_HOME + "/resources/ducc.properties")
        CMDPARMS.append('-Djava.library.path=' + self.DUCC_HOME)
        CMDPARMS.append('-Xmx100M')
        CMDPARMS.append('-XX:+HeapDumpOnOutOfMemoryError')
        
        ducc_heap_dump_path = self.ducc_properties.get('ducc.heap.dump.path')
        if ducc_heap_dump_path != None:
            CMDPARMS.append('-XX:HeapDumpPath='+ducc_heap_dump_path)
            
        CMDPARMS.append('-Dducc.agent.node.metrics.fake.memory.size=' + str(memory))
        # Put virtual IP on command line for duplicate daemon detector
        CMDPARMS.append('-Dducc.agent.virtual=' + ip)
        CMDPARMS.append('org.apache.uima.ducc.common.main.DuccService')
        
        print "Start agent with pnode", pnode, "IP", ip, "memory", memory
        os.environ['DUCC_NODENAME'] = pnode
        os.environ['DUCC_IP'] = ip

        self.nohup(CMDPARMS)
        #self.spawn(' '.join(CMDPARMS))

    #
    # Start admin components rm pm sm ws db or, on local node using Ducc.py
    #        
    def startComponent(self, args):

        msgs = []
        com, or_parms = args

        if ( com in ('ws', 'viz') ):
            node = self.webserver_node
        else:
            node = self.localhost

        if ( com == 'or' ):
            lines = self.ssh(node, True, "'", 
                             self.DUCC_HOME + '/admin/ducc.py', '-c', 'or', '-b', 
                             '--or_parms', or_parms, "'")
        else:
            lines = self.ssh(node, True, "'", 
                             self.DUCC_HOME + '/admin/ducc.py', '-c', com, '-b', "'")

        msgs.append(('Start', com, 'on', node))
        while 1:
            line = lines.readline().strip()
            if ( not line ):
                break
            #print '[] ' + line
            if ( line.startswith('PID') ):
                toks = line.split(' ')    # get the PID
                self.pidlock.acquire();
                self.pids.put(com, self.localhost + ' ' + toks[1] + ' ' + self.localhost)
                self.pidlock.release();
                lines.close()
                msgs.append(('    PID', toks[1]))
                break

        return msgs

    #
    # Read the special nodelist and start "special" agents
    #
    # Nodelist has records compatible with java properties like this:
    #
    #   index nodename memory-in-gb
    #
    # We generate fake IP addresses from 192.168.4.X where x is the index.
    # We generate fake node names from nodename-X where x is the index.
    # We pass the memory-in-gb to the agent as the fake memory it reports instead of real memory.
    #
    # We use Jerry's memory override for each agent to get it to falsely report the memory
    # instead of reading from the real machine.
    #

    def startOneAgent(self, args):
        response = []
        node, cmd, mem, ip, pnode, index = args
        response.append(('Starting agent on', node, 'instance', index, 'as pseudo-node', pnode, 'IP', ip, 'memory', mem))
        lines = self.ssh(node, True, "'", cmd, '--agent', '--memory', mem, '--addr', ip, '--pseudoname', pnode, "'")
        while 1:
            line = lines.readline().strip()
            if ( not line ):
                break
            #print '[]', line
            if ( line.startswith('PID')):
                toks = line.split(' ')    # get the PID
                lines.close()
                response.append(('  ... Started,  PID', toks[1]))

                # Gottoa run on old python that doesn't know 'with'
                self.pidlock.acquire()
                self.pids.put(index, node + ' ' + toks[1] + ' ' + pnode)
                self.pidlock.release()

                break
        return response
  
    def startAgents(self, nodelist, instances):        
        do_all = True
        if ( len(instances) > 0 ):
            do_all = False
            print 'Starting instances:', instances

        print "Starting from nodes in", nodelist
        props = Properties()
        props.load(nodelist)

        # first parse the node config into a list of tuples describing the simulated nodes
        # each tuple is like this: (index, nodename, mem)
        # where 'index' is a unique number (must be a number)
        #       'nodename' is the name of a real node, which will contain one or more agents
        #                  to simulate multiple nodes
        #       'mem' is the simulated memory the agent will report
        nodes = props.get('nodes').split()
        mems  = props.get('memory').split()

        allnodes = []
        ndx = 1
        for node in nodes:
            # allow the index to be reset by-node so you can vary the number of
            # pseudo nodes on a real node without changing the indexes of others
            # if desired, e.g. to manage a custom ducc.nodes
            key = 'index.' + node
            if ( props.has_key(key) ):
                ndx = int(props.get(key))

            # generate node names and associate simulated memory
            for mem in mems:
                count = props.get(node + '.' + mem)
                if ( count != None ):
                    for i in range(0, int(count)):
                        allnodes.append( (str(ndx), node, mem) )
                        ndx = ndx + 1
                            

        here = os.getcwd()
        cmd = os.path.abspath(sys.argv[0])   
        for (index, node, mem) in allnodes:
            if ( not do_all ):
                if ( not instances.has_key(index) ):
                    continue

            ip = '192.168.4.' + index
            pnode = node + '-' + index
            self.threadpool.invoke(self.startOneAgent, node, cmd, mem, ip, pnode, index)
            time.sleep(SLEEP_TIME)

    def usage(self, msg):
        if (msg != None):
            if ( msg[0] != None ):
                msg = ' '.join(msg)
            print msg
              
        print "Usage:"
        print "   start_sim [options]"
        print "   If no options are given this help screen is shown."
        print ""
        print "Options:"
        print "   -n --nodelist nodelist"
        print "        The nodelist describing agents that is to be used. Not valid with --agent, --memory, --agent, or --pseudoname."
        print "        A nodelist provides the parameters for starting agents. Lines are of the form:"
        print "           index nodename mem-in-GB"
        print "        'index' is any unique number."
        print "        'nodename' is the physical node you want the agent placed on"
        print "        'mem-in-GB' is the amount of simulated memory the agent will report"
        print ""
        print "        Example:"
        print "          1  node290 31"
        print "          2  node290 31"
        print "          9  node291 31"
        print "          10 node291 31"
        print "          17 node292 47"
        print "          18 node292 47"
        print "          25 node293 47"
        print "          26 node293 47"
        print ""
        print "   -c --components component"
        print "        Start the indicated component, must be one of", ' '.join(self.default_components), "or 'all'"
        print ""
        print "   -i --instance instanceid"
        print "        Start only this instance of an agent from the nodelist."
        print ""
        print "   --agent"
        print "        Start an agent only on localhost. All of --memory, --addr, and --pseudoname are required, -n is disallowed."
        print ""
        print "   --memory mem"
        print "        Use this memory override.  Valid only with --agent."
        print ""
        print "   --addr"
        print "        Use this IP override. Valid only with --agent."
        print ""
        print "   --pseudoname pseudoname"
        print "        Use this as the hostname for the agent. Valid only with -a."
        print ""
        print "    --nothreading"
        print "        Disable multithreaded operation if it would otherwise be used"
        print ""
        print "   -v, --version"
        print "        Print the current DUCC version"
        sys.exit(1)


    def invalid(self, *msg):
        if ( msg != None ):
            if ( msg[0] != None ):
                msg = ' '.join(msg)
        print msg

        print "For usage run"
        print "    start_sim -h"
        print 'or'
        print '    start_sim --help'
        sys.exit(1)

    def main(self, argv):
        print "Running as", os.getpid()

        if ( len(argv) == 0 ):
            self.usage(None)
    
        node_config = None
        components = {}
        instances = {}
        run_agent = False
        memory = None
        pseudoname = None
        IP = None

        or_parms = self.ducc_properties.get('ducc.orchestrator.start.type')

        try:
            opts, args = getopt.getopt(argv, 'c:i:n:vh?', ['component=', 'help', 'agent', 'memory=', 
                                                          'instance=', 'addr=', 'pseudoname=', 'node-config=', 
                                                          'version', 'warm', 'cold',
                                                           'nothreading'])
        except:
            self.invalid('Invalid arguments', ' '.join(argv))
                  
        for ( o, a ) in opts:
            if o in ( '-n', '--node-config' ): 
                node_config = a
            elif o in ( '--agent' ):
                run_agent = True
            elif o in ( '-c', '--component' ):
                if ( a == 'all' ):
                    for cmp in self.default_components:
                        components[cmp] = cmp
                else:
                    components[a] = a
            elif o in ( '--memory' ):
                memory = a
            elif o in ( '--addr' ):
                IP = a
            elif o in ( '-i', '--instance' ):
                instances[a] = a
            elif o in ( '--pseudoname' ):
                pseudoname = a
            elif o in ( '--nothreading' ):
                self.disable_threading()
            elif o in ( '-v', '--version' ):
                print self.version()
                os.exit(0)
            elif o in ( '--warm', '--cold' ):
                or_parms = o[2:]
            elif o in ( '-h', '--help' ):
                self.usage(None)
            elif ( o == '-?'):
                self.usage(None)
            else:
                self.invalid('bad args: ', ' '.join(argv))

        self.pids = Properties()
        self.ducc = Ducc()

        if ( not self.verify_jvm() ):
            return  

        self.set_duccling_version()
        self.verify_duccling()

        if ( os.path.exists('sim.pids') ):
            self.pids.load('sim.pids')
        if ( run_agent ):
            #
            # checks that aren't valid if we want run_agent
            #
            if ( node_config != None ):
                self.invalid("Nodelist is not compatible with agent")
                
            if ( (IP == None ) or ( memory == None) or ( pseudoname == None )):
                    self.invalid("Missing IP, memory, or pseudoname")

            self. run_local_agent(pseudoname, IP, memory)
            sys.exit(0)
        else:         
            self.pidlock = Lock()
            self.threadpool = ThreadPool(50)

            if ( (IP != None) or (memory != None) or ( pseudoname != None )) :
                self.invalid("Running with a nodelist is not compatible with running a single agent.");

            try:
                specials = ['broker', 'db']
                sc = set(components)
                sb = set(specials)
                specials_only = False
                if ( sc.issubset(sb) ):
                    read_pids = True

                print '-------- start broker'
                if ( components.get('broker') != None ):
                    self.start_broker()
    
                print '-------- start database'

                if ( components.get('db') != None ):
                	self.start_database()
                        
                print '-------- specials_only', specials_only
                if ( specials_only ):
                    return

                print '-------- start agents'
                if ( node_config != None ):
                    self.startAgents(node_config, instances)
    
                for (com, com) in components.items():
                    if ( not com in specials ):    # specials start with different rules
                        self.threadpool.invoke(self.startComponent, com, or_parms)
                        time.sleep(SLEEP_TIME)

            except:
                pass

            self.threadpool.quit()
            self.pids.write('sim.pids')            

if __name__ == "__main__":
    starter = StartSim()
    starter.main(sys.argv[1:])

