#!/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.
# -----------------------------------------------------------------------

import os
import sys
import getopt
import platform

import string
import random

import shutil
import subprocess
from  stat import *

# DuccUtil adds ../bin to the path so ducc_base can be found
from ducc_util import DuccUtil
from ducc_base import Properties

from ducc_base import find_ducc_home
from ducc_base import find_localhost
from ducc_base import which

from ducc import Ducc
import db_util as dbu

class PostInstall():

    def usage(self, msg):

        if ( msg != None ):
            print ' '.join(msg)

              
        print "Usage:"
        print "   ducc_post_install [options]"
        print "        If no options prompts are given for expected parameters."
        print ""
        print "Options:"
        print "   [-n, --head-node] <ducc head node>"
        print "        This is the name of the host that will run the DUCC management processes."
        print ""
        print "   [-k, --keystore] <webserver keystore password>"
        print "        This is the password to be used to establish the webserver's keystore."
        print ""
        print "   [-j, --jvm] <path to java executable>"
        print "        This is the full path to java command to be used to start DUCC; e.g., /usr/bin/java"
        print ""
        print "   [-a, --db-automanage] <True|False>"
        print "        Specify False if DUCC should not start/stop database."
        print ""
        print "   [-r, --db-replication] <integer>"
        print "        Specify database replication level (default = 1)."
        print ""
        print "   [-m, --db-home] <path>"
        print "        The location of the database home directory, only specify when db-automanage is False and the default database directory should not be used."
        print ""
        print "   [-o, --db-host-list] host1 <host2 host3...>"
        print "        The database host(s)."
        print ""
        print "   [-u, --db-user] <root user for database>"
        print "        This is the user DUCC uses to manage the database."
        print ""
        print "   [-d, --db-password] <root password for database>"
        print "        This is the password DUCC uses to manage the database."
        print ""
        print "   [-b, --br-password] <password for broker>"
        print "        This is the password DUCC uses to manage the broker."
        print ""
        print "   [-h, -? --help]"
        print "        Prints this message."
        print ""
        sys.exit(1) 
            

    def fail(self, *msg):
        print ' '.join(msg)
        print "POST INSTALLATION FAILED"
        sys.exit(1)


    def warn(self, *msg):
        print ''
        print 'WARNING'
        print 'WARNING', ' '.join(msg)
        print 'WARNING'
        print ''

    def addToCp(self, cp, lib):
        return cp + ':' + self.DUCC_HOME + '/' + lib

    def execute(self, CMD):
        print CMD
        rc = os.system(CMD)
        if ( rc != 0 ):
            print 'Failure, cannot continue.'
            sys.exit(1)

    # set username and password in broker credentials file        
    def configure_broker(self):
        cf = self.DUCC_HOME+"/resources.private/ducc-broker-credentials.properties"
        # create file if it does not exist
        if(self.broker_pw == None):
            self.broker_pw = self.generate_pw()
        if ( not os.path.exists(cf) ):
            # create file with username & password
            print "broker configuration create"
            with open(cf, 'w') as f:
                line = 'ducc.broker.admin.username=admin'+'\n'
                print line
                f.write(line)
                line = 'ducc.broker.admin.password='+self.broker_pw+'\n'
                print line
                f.write(line)
        # update existing file
        else:
            # re-write file replacing username & password
            print "broker configuration edit"
            with open(cf, 'r+') as f:
                lines = f.readlines()
                f.seek(0)
                f.truncate()
                for line in lines:
                    if 'ducc.broker.admin.username=' in line:
                        line = 'ducc.broker.admin.username=admin'+'\n'
                        print line
                    if 'ducc.broker.admin.password=' in line:
                        line = 'ducc.broker.admin.password='+self.broker_pw+'\n'  
                        print line
                    f.write(line)
        return
        
    def setup_database(self):
        # for cassandra:
        # in ducc_runtime/cassandra-server/conf we need to update cassandra.yaml to establish
        # the data directories and db connection addresses

        # Note this is a bootstrap routine and doesn't try to use common code that may depend on
        # things being initialized correctly.

        # If are re-running the DB may already have been created so use the saved password
        db_pw = self.ducc_private_properties.get('db_password')
        if (db_pw == None):
            db_pw = self.get_pw(self.database_pw)

        #if ( db_pw == 'bypass' ):
        #    print 'Database support will be disabled'
        #    self.update_property('ducc.database.host', '--disabled--', '# Database support is disabled')
        #    return True;
        
        if ( dbu.configure_database(self.DUCC_HOME, self.ducc_head, self.path_to_java, self.database_automanage, self.database_host_list, self.database_user, db_pw, self.database_replication) ):
            print 'Configuring DUCC to use the database.'

            self.update_property('ducc.service.persistence.impl', 'org.apache.uima.ducc.database.StateServicesDb', '# Service manager persistence')
            self.update_property('ducc.job.history.impl', 'org.apache.uima.ducc.database.HistoryManagerDb', '# History and checkpoint')
            self.update_property('ducc.rm.persistence.impl', 'org.apache.uima.ducc.database.RmStatePersistence', '# RM state persistence')

            self.ducc_private_properties.delete('db_password')
            self.ducc_private_properties.put('db_password', db_pw, ['#Db password, default is randomly generated']);
            
            self.ducc_private_properties.delete('db_password_guest')
            self.ducc_private_properties.put('db_password_guest', db_pw, ['#Db password for guest, default is same value as db_password']);
            
            return True
        else:
            return False

    # generate a random string between 8 and 16 characters long
    def generate_pw(self):
        pwlen = random.randrange(8,16)
        reply = ''.join([random.choice(string.ascii_letters + string.digits) for n in xrange(pwlen)])
        return reply
    
    # if password is not specified then generate a random one
    def get_pw(self, given):
        reply = given
        if ( given == None ):
            reply = self.generate_pw()
        return reply

    def create_keystore(self, keytool):

        '''
        CN - Common Name of the certificate owner
        OU - Organizational Unit of the certificate owner
        O - Organization to which the certificate owner belongs
        L - Locality name of the certificate owner
        S - State or province of the certificate owner
        C - Country of the certificate owner
        '''
        
        keystore = self.DUCC_HOME + "/webserver/etc/keystore"
        cmd = 'rm ' + keystore
        os.system(cmd);

        #/usr/bin/keytool
        keystore_key = 'ducc.ws.port.ssl.pw'
        self.default_keystore_prop = self.ducc_private_properties.get_property(keystore_key)
        self.default_keystore_pw = self.default_keystore_prop.k

        rc = 1
        reply = ''
        while ( rc != 0 ):
            reply = self.get_pw(self.keystore_pw)

            cmd = keytool
            cmd += ' '
            cmd += '-genkey'
            cmd += ' '
            cmd += '-noprompt'
            cmd += ' '
            cmd += '-alias jetty'
            cmd += ' '
            cmd += '-dname "CN=org.apache.uima.ducc, OU=uima.ducc, O=Apache, L=Wilmington, S=Delaware, C=USA"'
            cmd += ' '
            cmd += '-keyalg RSA'
            cmd += ' '
            cmd += '-validity 10000'
            cmd += ' '
            cmd += '-keystore ' + keystore
            cmd += ' '
            cmd += '-storepass '+ reply
            cmd += ' '
            cmd += '-keypass '+ reply
            rc = os.system(cmd);

        self.default_keystore_prop.v = reply
        print 'keystore = ', keystore
        #print 'keypass = ', reply
        #print 'storepass = ', reply
    # Setup and verify amq
    # make sure verify_ducc is sufficient - maybe move some checks to there?

    def update_property(self, key, val, comment):
        self.ducc_site_properties.put(key, val, [comment])

    def get_java_bindir(self):

        if ( self.path_to_java == None ):

            self.path_to_java = which('java')
            if ( self.path_to_java == None ):
                reply = ''
                while ( reply == '' ):
                    reply = raw_input('Enter full path to the Java executable: ')
            else:
                prompt = '[' + self.path_to_java + ']'
                reply = raw_input('Enter full path to the Java executable: ' + prompt)

            if ( reply != '' ):       
                self.path_to_java = reply

        # automagically fix // in path to java
        self.path_to_java = self.path_to_java.replace('//', '/')

        self.update_property('ducc.jvm', self.path_to_java, '# The full path to java')
        print 'Initialized property "ducc.jvm" to', self.path_to_java

        # We're going to do more checks here so we don't proceed with bogosities
        proc = subprocess.Popen(self.path_to_java + ' -version', shell=True, bufsize=0, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        lines = []
        for  line in proc.stdout:
            lines.append(line.strip())
        proc.wait()
        rc = proc.returncode

        for line in lines:
            print "JAVA: " + line

        vertoks = lines[0].split()
        self.java_version = vertoks[-1]

        if ( rc != 0 ):
            self.fail("Requested java at '" + self.path_to_java +"' cannot be run.")

        return os.path.dirname(self.path_to_java)

    def set_java_home(self):
        jvm = self.path_to_java
        if ( platform.system() == 'Darwin' ):
            self.jvm_home = "/Library/Java/Home"
        else:
            ndx = jvm.rindex('/')
            ndx = jvm.rindex('/', 0, ndx)
            self.jvm_home = jvm[:ndx]

        os.environ['JAVA_HOME'] = self.jvm_home

    def get_java_version(self):
        return self.java_version

    def get_java_keytool(self, bindir):
        keytool = bindir + "/keytool"
        if ( not os.path.exists(keytool) ):
            self.fail("Cannot find keytool in ", bindir + '.', + "Is ducc.jvm configured correctly?")
        return keytool

    def check_nodes(self):
        nodes = self.DUCC_HOME + "/resources/ducc.nodes"
        self.mkbackup(nodes)

        nf = open(nodes, 'w')
        nf.write(self.localhost)
        nf.close()
        print "Initial", nodes, "created."
            
    def merge_properties(self):
        # first task, always, merge the properties so subsequent code can depend on their validity.
        base_props = self.DUCC_HOME + '/resources/default.ducc.properties'
        site_props = self.DUCC_HOME + '/resources/site.ducc.properties'
        run_props = self.DUCC_HOME + '/resources/ducc.properties'
        merger = self.DUCC_HOME + '/admin/ducc_props_manager'
        CMD = [merger, '--merge', base_props, '--with', site_props, '--to', run_props]
        CMD = ' '.join(CMD)
        print 'Merging', base_props, 'with', site_props, 'into', run_props
        os.system(CMD)
            
    def setup_ducc_uid(self):  
        self.ducc_uid = os.environ['LOGNAME']
        self.update_property('ducc.uid', self.ducc_uid, "# ducc.uid is the user that runs the main DUCC daemons");
        print "Ducc uid is configured as", self.ducc_uid, '\n'           
            
    def setup_ducc_head(self):

        if ( self.ducc_head == None ):
            self.ducc_head = self.localhost
            reply = raw_input('Enter hostname of DUCC head[' + self.ducc_head + ']')
            if ( reply != '' ):
                self.ducc_head = reply

        self.update_property('ducc.head', self.ducc_head, "# ducc.head is the node where the main DUCC daemons run");

        print "Ducc head is configured as", self.ducc_head, '\n'        

    def setup_ducc_database(self):
        # ducc.database.automanage
        if(self.database_automanage == None):
            if ( self.database_host_list == None ):
                # db automanage default to T when db host list not specified
                self.database_automanage = True
            else:
                # db automanage default to F when db host list specified
                self.database_automanage = False
        if(self.database_automanage == False):
            self.update_property('ducc.database.automanage', 'False', '# Database start/stop not managed by DUCC')
        else:
            self.update_property('ducc.database.automanage', 'True', '# Database start/stop managed by DUCC')
        # ducc.database.host.list
        if ( self.database_host_list == None ):
            self.database_host_list = self.ducc_head
        self.update_property('ducc.database.host.list', self.database_host_list, '# Database host list') 
    	# ducc.database.user
        if ( self.database_user == None ):
            self.database_user = 'ducc'
        self.update_property('ducc.database.user', self.database_user, '# Database user') 
        # ducc.database.home
        if ( self.database_home != None ):
            if(self.database_automanage == False):
                self.update_property('ducc.database.home', self.database_home, '# Database home')
            else:
                print "db-home ignored", self.database_home, '\n'  
            
    def mkbackup(self, fn):
        if ( os.path.exists(fn) ):
            bak = fn + '.bak'
            shutil.move(fn, bak)
            print 'Existing', fn, 'moved to', bak
    
    def verify_permissions(self):
        # should have 755 permissions
        spot_checked_directories = ['../bin', '../lib', '../resources' ]
        # should have 755 permissions
        spot_checked_execs       = ['../bin/ducc_submit']
        # should have 644 permissions
        spot_checked_data        = ['../lib/uima-ducc-cli.jar', '../resources/default.ducc.properties']

        ret = True
        for f in spot_checked_directories:
            if ( not os.path.exists(f) ):
                print 'ERROR: Directory', f, 'cannot be found.'
                ret = False
                continue

            stat = os.stat(f)
            mode = oct(stat.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO))
            expected = oct(0755)
            if ( mode != expected ):
                print 'ERROR: Directory', f, 'has permissions', mode, 'expected', expected
                ret = False

        for f in spot_checked_execs:
            if ( not os.path.exists(f) ):
                print 'ERROR: File', f, 'cannot be found.'
                ret = False
                continue

            stat = os.stat(f)
            mode = oct(stat.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO))
            expected = oct(0755)
            if ( mode != expected ):
                print 'ERROR: File', f, 'has permissions', mode, 'expected', expected
                ret = False

        for f in spot_checked_data:
            if ( not os.path.exists(f) ):
                print 'ERROR: File', f, 'cannot be found.'
                ret = False
                continue

            stat = os.stat(f)
            mode = oct(stat.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO))
            expected = oct(0644)
            if ( mode != expected ):
                print 'ERROR: File', f, 'has permissions', mode, 'expected', expected
                ret = False

        return ret
        
    def verify_prereqs(self):
        fn = '/usr/bin/nslookup'
        # will throw exception if not found
        dir_stat = os.stat(fn)

    def main(self, argv):                    

        self.verify_prereqs()
        self.DUCC_HOME = find_ducc_home()
        self.localhost = find_localhost()
        cwd = os.getcwd()
        if cwd != self.DUCC_HOME+'/admin':
            print '>>> ERROR - this script must be run from the admin directory'
            sys.exit(99)
        print 'Using DUCC HOME:', self.DUCC_HOME, '\n'

        if ( not self.verify_permissions() ):
            print '--------------------------------------------------------------------------------'
            print 'Package verificaiton fails.  Most likely cause is an unexpected UMASK unpacking the distribution.'
            print 'To unpack the distribution your UMASK must be set to 022.'
            print ''
            print 'Example:'
            print ''
            print 'umask 022; tar -xf [distribution]'
            print '--------------------------------------------------------------------------------'
            sys.exit(1)

        self.ducc_uid = None
        self.ducc_head = None
        self.keystore_pw = None
        self.database_automanage = None
        self.database_replication = None
        self.database_home = None
        self.database_host_list = None
        self.database_user = None
        self.database_pw = None
        self.broker_pw = None
        self.path_to_java = None

        try:
            opts, args = getopt.getopt(argv, 'a:r:m:o:u:d:b:j:k:n:h?', ['db-automanage=', 'db-replication=', 'db-home=', 'db-host-list=', 'db-user=', 'db-password=', 'br-password=', 'jvm=', 'keystore=', 'head-node=', 'help'])
        except:
            self.usage("Invalid arguments " + ' '.join(argv))

        for ( o, a ) in opts:
            if o in ('-n', '--head-node'):
                self.ducc_head = a
            if o in ('-a', '--db-automanage'):
                if(o in [ 'True', 'T', 'true', 't']):
                    self.database_automanage = True
                else:
                    self.database_automanage = False
            if o in ('-r', '--db-replication'):
                self.database_replication = a
            if o in ('-m', '--db-home'):
                self.database_home = a
            if o in ('-o', '--db-host-list'):
                self.database_host_list = a
            if o in ('-u', '--db-user'):
                self.database_user = a
            if o in ('-d', '--db-password'):
                self.database_pw = a
            if o in ('-b', '--br-password'):
                self.broker_pw = a
            if o in ('-k', '--keystore'):
                self.keystore_pw = a
            if o in ('-j', '--jvm'):
                self.path_to_java = a
            elif o in ('-h', '-?', '--help'):
                self.usage(None)
        
        resources = self.DUCC_HOME + '/resources'
        self.site_properties_name = resources + '/site.ducc.properties'
        self.ducc_properties = resources + '/ducc.properties'

        if(os.path.exists(self.ducc_properties)):
            text0 = '\nDUCC is already installed!'
            text1 = '\nTo re-run ducc_post install be sure DUCC is stopped using "stop_ducc --all"'
            text2 = "\nand back-up any DUCC configuration files that will be re-used,"
            text3 = '\nthen re-install DUCC.'
            self.fail(text0+text1+text2+text3)

        self.mkbackup(self.site_properties_name)
        self.mkbackup(self.ducc_properties)

        print 'Python version:'
        print sys.version

        keystore_properties_name = self.DUCC_HOME + '/resources.private/ducc.private.properties'
        self.ducc_private_properties = Properties()
        self.ducc_private_properties.load(keystore_properties_name)
 
        self.ducc_site_properties = Properties()

        py_version = sys.version_info
        if ( (py_version[0] != 2) or (py_version[1] < 4) ):
            self.fail("\nPython must be installed at level 2.4 or higher.")

        self.setup_ducc_uid()
        self.setup_ducc_head()
        self.setup_ducc_database()
        self.check_nodes()

        # insure java is configured and installed
        self.java_bindir = self.get_java_bindir()
        self.set_java_home()
        print "java_home", os.environ['JAVA_HOME']

        # As of DUCC 2.0, always set here on installation
        print 'ActiveMQ is automanaged on node ', self.localhost

        statedir = self.DUCC_HOME + "/state"
        logdir = self.DUCC_HOME + "/logs"
        #logwsdir = self.DUCC_HOME + "/logs/webserver"
        historydir = self.DUCC_HOME + "/history"
        if ( not os.path.exists(statedir) ):
            os.mkdir(statedir)
        if ( not os.path.exists(logdir) ):
            os.mkdir(logdir)
        #if ( not os.path.exists(logwsdir) ):
        #    os.mkdir(logwsdir)
        if ( not os.path.exists(historydir) ):
            os.mkdir(historydir)

        # configure the AMQ broker
        self.configure_broker()

        # configure the database for local system and initialize the schema
        if not self.setup_database():
            print 'Database creation failed - DUCC setup incomplete'
            sys.exit(1)

        self.keytool = self.get_java_keytool(self.java_bindir)
        print 'Java version:', self.get_java_version()
        print 'Java is verified.'

        self.create_keystore(self.keytool)
        print '\nWeb server keystore generated from ducc.properties'

        ws_duccbook = self.DUCC_HOME + "/webserver/root/system.duccbook.html"
        if ( not os.path.lexists(ws_duccbook) ):
            duccbook = self.DUCC_HOME + "/docs/book.html"
            os.symlink(duccbook, ws_duccbook)
        print '\nDUCC book installed into webserver root\n'

        # set up the local properties, required to build ducc_ling
        self.ducc_private_properties.write(keystore_properties_name)
        self.ducc_site_properties.write(self.site_properties_name)
        self.merge_properties()

        # Make duccling
        rc = os.system(self.DUCC_HOME + '/admin/build_duccling')
        if ( rc != 0 ):
            print 'Could not build ducc_ling.  Run the command'
            print '   build_duccling'
            print 'to complete the installation (it must run without error).'
            sys.exit(1)

        print 'Initial DUCC setup complete.'

if __name__ == "__main__":

    os.environ['DUCC_POST_INSTALL'] = 'DUCC_POST_INSTALL'
    postinstall = PostInstall()
    postinstall.main(sys.argv[1:])
