#! /usr/bin/python

import errno
import os
import getpass
import time
import signal
import sys
import traceback
from syslog import syslog, LOG_ERR, LOG_INFO, LOG_DEBUG, LOG_IFS

import isi.config.array as array
import isi.config.compliance as comp
import isi.config.licensing as lic
import isi.misc as misc
import isi.ui.lib.format as fmt
import isi.net.rpc as rpc
import isi.sys.update.util as updateutil

MLOCK_FILE = '/ifs/.ifsvar/run/isi_make_compliance.lock'
mlock_fd = -1

action_list = []
current_action = 0
comp_rpc = None
node_ops = None
compadmin_passwd = ''

def syslog_and_print(loglevel, logstr):
    print logstr
    syslog(loglevel, logstr)

# Try and verify a condition cluster-wide using an RPC function that returns
# a bool result for each node.

def rpc_verify(rpc, checker_name, exp_result = True):
    checker_func = getattr(rpc, checker_name)
    res = checker_func()

    for (node, nodeval) in res.iteritems():
        if nodeval != exp_result:
            return False

    return True

# Try and verify a condition cluster-wide using an RPC function that returns
# a bool result for each node. This function will try max_retries times to get
# a True result cluster-wide and return False if max_retries elapses.
# Each retry attempt sleeps 1 second.

def rpc_verify_with_retry(rpc, checker_name, max_retries = 30,
        exp_result = True):
    for num_retries in xrange(0, max_retries):
        if rpc_verify(rpc, checker_name, exp_result):
            return True

        time.sleep(1.0)

    return False

# Try to do an action locally first (seeing if MCP will propagate changes).
# action_args is a list of args to pass into the call to action_name.
# Execute checker function across all nodes to verify action succeeded. If
# not, try to execute action across all nodes, then run checker again.

def do_local_or_rpc_action(localobj, rpc, action_name, action_args,
        checker_name, exp_result = True):
    local_action = getattr(localobj, action_name)
    local_action(*action_args)
    if not rpc_verify_with_retry(rpc, checker_name, 10, exp_result):
        syslog(LOG_DEBUG, 'Action %s failed locally. Trying on all nodes.' %
                action_name)
        rpc_action = getattr(rpc, action_name)
        rpc_action(*action_args)
        if not rpc_verify_with_retry(rpc, checker_name, 20, exp_result):
            raise Exception('Action %s failed.' % action_name)
    else:
        syslog(LOG_DEBUG, 'Local action %s succeeded!' % action_name)

# Disable LSASS on all nodes so that pw command can directly edit
# /etc/master.passwd.

class DisableLSASSAction():

    def do(self):
        syslog_and_print(LOG_INFO, 'Disabling LSASS on all nodes...')
        comp_rpc.disable_lsass()
        syslog_and_print(LOG_INFO, 'Disabled LSASS on all nodes...')

    def undo(self):
        syslog_and_print(LOG_INFO, 'Enabling LSASS on all nodes...')
        comp_rpc.enable_lsass()
        syslog_and_print(LOG_INFO, 'Enabled LSASS on all nodes...')

class BackupComplianceFilesAction():

    def do(self):
        syslog_and_print(LOG_INFO, 'Backing up relevant files...')
        comp_rpc.backup_compliance_files()
        syslog_and_print(LOG_INFO, 'Backed up relevant files...')

        # XXXDPL Store local versions of all files to verify post restore!

    def undo(self):
        syslog_and_print(LOG_INFO, 'Restoring relevant files...')
        comp_rpc.restore_compliance_files()
        syslog_and_print(LOG_INFO, 'Restored relevant files...')


class EnableCompadminAction():

    def do(self):
        syslog_and_print(LOG_INFO, 'Configuring compadmin user...')
        localobj = comp.ComplianceProxy()

        do_local_or_rpc_action(localobj, comp_rpc, 'enable_compadmin_user',
                [compadmin_passwd], 'compadmin_enabled')

        syslog_and_print(LOG_INFO, 'Configured compadmin user...')

    def undo(self):
        # Fall through to BackupComplianceFilesAction::undo(), which restores
        # /etc/master.passwd
        return

class ConfigureSudoersAction():
    def do(self):
        syslog_and_print(LOG_INFO, 'Configuring sudoers...')
        localobj = comp.ComplianceProxy()

        do_local_or_rpc_action(localobj, comp_rpc, 'make_sudoers_compliant', [],
                'is_sudoers_compliant')

        syslog_and_print(LOG_INFO, 'Configured sudoers...')

    def undo(self):
        # Fall through to BackupComplianceFilesAction::undo(), which restores
        # /usr/local/etc/sudoers
        return

class MakeArrayCompliantAction():

    def do(self):
        syslog_and_print(LOG_INFO, 'Updating cluster config...')
        comp_rpc.set_array_config_compliant(True)
        if not rpc_verify_with_retry(comp_rpc, 'is_array_config_compliant'):
            raise Exception('Could not configure cluster for Compliance mode.')

        syslog_and_print(LOG_INFO, 'Updated cluster config...')

    def undo(self):
        syslog_and_print(LOG_INFO, 'Rolling back cluster config...')
        comp_rpc.set_array_config_compliant(False)
        if not rpc_verify_with_retry(comp_rpc, 'is_array_config_compliant',
                exp_result = False):
            raise Exception('Could not roll back cluster config.')

        syslog_and_print(LOG_INFO, 'Rolled back cluster config...')

class SetConsoleInsecureAction():
    def do(self):
        syslog_and_print(LOG_INFO, 'Disabling single-user mode...')
        comp_rpc.set_ttys_console_secure(False)
        if not rpc_verify_with_retry(comp_rpc, 'verify_ttys_file_compliant'):
            raise Exception('Could not disable single-user mode.')

        syslog_and_print(LOG_INFO, 'Disabled single-user mode...')

    def undo(self):
        # Fall through to BackupComplianceFilesAction::undo(), which restores
        # /etc/ttys
        return

class DisableRootAction():
    def do(self):
        syslog_and_print(LOG_INFO,'Disabling root login...')
        localobj = comp.ComplianceProxy()
        do_local_or_rpc_action(localobj, comp_rpc, 'make_passwd_compliant', [],
                'verify_passwd_compliant')

        syslog_and_print(LOG_INFO,'Disabled root login...')

    def undo(self):
        # Fall through to BackupComplianceFilesAction::undo(), which restores
        # /etc/master.passwd
        return

class EnableLSASSAction():

    def do(self):
        syslog_and_print(LOG_INFO, 'Enabling LSASS on all nodes...')
        comp_rpc.enable_lsass()
        syslog_and_print(LOG_INFO, 'Enabled LSASS on all nodes...')

    def undo(self):
        syslog_and_print(LOG_INFO, 'Disabling LSASS on all nodes...')
        comp_rpc.disable_lsass()
        syslog_and_print(LOG_INFO, 'Disabled LSASS on all nodes...')

def get_worm_license_key():
    print 'Please enter a valid SmartLock Compliance license.'
    print '(format: XXXXX-XXXXX-XXXXX-XXXXX-XXXXX)'
    print '>>> ',
    line = raw_input()
    if line == 'skip':
        return True

    if lic.valid_key_for_module(line, 'SmartLock'):
        return True

    return False

def get_compadmin_passwd():
    print 'Please change the compliance admin password from the default.'
    while True:
        pass1 = getpass.getpass('Please enter new password for compadmin: ')
        if pass1 == '':
            continue

        pass2 = getpass.getpass('Please re-enter password for compadmin: ')
        if pass1 != pass2:
            print '\nYou did not enter the same password.\n'
            continue

        if len(pass1) > 40:
            print '\nThe password you entered is too long.'
            continue

        return pass1

def init_rpc(rpc_class):
    rpc_obj = rpc.RPCDaemonClusterProxyWithTimeout(node_ops, rpc_class, None)

    failed_conns = rpc_obj.failedConnections()
    if failed_conns:
        fail_list = ""
        for (node, reason) in failed_conns.iteritems():
            fail_list += 'Node %d: %s\n' % (node.lnn(), reason)
        raise Exception('Failed to instantiate RPC object for %s: %s' %
                (rpc_class, fail_list))
    return rpc_obj

def init_action_list():
    global action_list

    action_list.append(DisableLSASSAction())
    action_list.append(BackupComplianceFilesAction())
    action_list.append(EnableCompadminAction())
    action_list.append(ConfigureSudoersAction())
    action_list.append(MakeArrayCompliantAction())
    action_list.append(SetConsoleInsecureAction())
    action_list.append(DisableRootAction())
    action_list.append(EnableLSASSAction())

def rollback_actions():
    syslog_and_print(LOG_ERR | LOG_IFS, 'Rolling back configuration.\n')

    for i in xrange(current_action, -1, -1):
        try:
            action_list[i].undo()
        except KeyboardInterrupt:
            print 'Caught Ctrl-C. Please wait until rollback completes.'
            pass
        except Exception, e:
            syslog(LOG_ERR | LOG_IFS, traceback.format_exc())
            syslog_and_print(LOG_ERR | LOG_IFS, str(e))
            pass

def do_action_list():
    global current_action
    try:
        for current_action in xrange(0, len(action_list)):
            action_list[current_action].do()
    except Exception, e:
        syslog(LOG_ERR | LOG_IFS, traceback.format_exc())
        syslog_and_print(LOG_ERR | LOG_IFS, 'ERROR: ' + str(e))
        rollback_actions()
        return False

    return True

def get_master_pidfile_lock():
    global mlock_fd

    try:
        mlock_fd = os.open(MLOCK_FILE, os.O_CREAT | os.O_WRONLY | os.O_EXLOCK | 
                os.O_NONBLOCK)
    except OSError, e:
        if e.errno == errno.EAGAIN:
            raise Exception('Already running.')
        else:
            raise

def release_master_pidfile_lock():
    global mlock_fd
    if mlock_fd > 0:
        os.close(mlock_fd)
    mlock_fd = -1

def main():
    global node_ops
    global comp_rpc
    global compadmin_passwd
    global action_list

    try:
        get_master_pidfile_lock()
    except Exception, e:
        print e
        return 1

    if os.getuid() != 0:
        print 'You must be root to do that.'
        return 1

    if misc.sysctlbyname('isi.compliance') == 1:
        print 'Already in Compliance mode.'
        return 1

    node_ops = array.NodeOps()

    try:
        comp_rpc = init_rpc('isi.config.compliance.ComplianceProxy')

    except Exception, e:
        print e
        return 1

    if not get_worm_license_key():
        print 'Not a valid SmartLock Compliance license key.'
        return 1

    conf = fmt.prompt_user('Are you sure? Once you switch this cluster to ' +
            'SmartLock Compliance mode, you cannot switch it back!')
    if conf != 'yes':
        return 0

    compadmin_passwd = get_compadmin_passwd()
    init_action_list()

    conf = ''

    try:
        if do_action_list():
            # Duplicate most of the functionality in
            # isi.ui.lib.format.get_user_choice() since we need to catch
            # Ctrl-C/D and rollback instead of directly exit.

            choices = ('yes', 'no')
            print fmt.fmtUserMsg('All nodes configured for Compliance ' + \
                    'operation. Enter \'yes\' to confirm switch to ' + \
                    'Compliance mode and reboot, or \'no\' to ' + \
                    'abort and rollback to previous configuration.',
                    choices)
            while True:
                conf = raw_input('>>> ')
                if conf in choices or conf == '':
                    break

                print 'Please enter one of \'yes\' or \'no\'.'

            if conf == 'yes':
                signal.signal(signal.SIGINT, signal.SIG_IGN)
                rbtr = updateutil.Rebooter()
                rbtr.cluster_reboot()
            else:
                rollback_actions()

        else:
            return 1

    except KeyboardInterrupt:
        print '\nCaught Ctrl-C.'
        rollback_actions()
        return 1

    except EOFError:
        print '\nCaught Ctrl-D.'
        rollback_actions()
        return 1

    return 0

if __name__ == '__main__':
    exitcode = main()
    release_master_pidfile_lock()
    sys.exit(exitcode)
