#!/usr/bin/python
# Moby AIMA upgrade script

import logging
import os
import re
from socket import gethostname
import subprocess
import sys
import syslog
import time

from isi.app.lib import safelockfile
import isi.app.lib.upgrade_api as uapi
import isi.auth_util as auth_util
import isi.sys.version as sysver


CLI_PATH = "/usr/local/share/CLI/"
sys.path.append(CLI_PATH)

import papi.papi as papi
import papi.rest as rest
import papi.conn as conn
import papi.container as container

IFS_BACKUP_PATH = "/ifs/.ifsvar/tmp"
LOG_FILE = os.path.join(IFS_BACKUP_PATH, "moby-auth-upgrade.log")
AUTH_UPGRADE_FILE = os.path.join(IFS_BACKUP_PATH, "moby-auth-upgraded")
AUTH_UPGRADE_DONE_FILE = os.path.join(IFS_BACKUP_PATH, "moby-auth-upgraded-done")

SYSCTL_CONF_PATH = "/etc/mcp/override"
SYSCTL_CONF_FILE = "sysctl.conf"
SYSCTL_CONF = os.path.join(SYSCTL_CONF_PATH, SYSCTL_CONF_FILE)

logger = logging.getLogger("moby_auth_upgrade")

def krb5_get_realm(keytab, logger):
    try:
        spnlist = auth_util.krb5_get_keytab_spns(keytab)
    except:
        logger.info("%s: Could not open keytab %s, skipping" %
                    (time.ctime(), keytab))
        return None

    realm = None

    if spnlist:
        for spn in spnlist:
            try:
                r = re.search('@(.+)\Z', spn)
                this_realm = r.group(1)
            except Exception, e:
                raise RuntimeError("%s: Krb5: cannot parse spn %s, error %s" %
                                   (time.ctime(), spn, str(e)))
            if realm:
                if this_realm != realm:
                    logger.error("%s: Keytab %s has >1 realms: %s and %s" %
                                 (time.ctime(), keytab, realm, this_realm))
                    raise RuntimeError("%s: Krb5: multiple realms in keytab %s" %
                                       (time.ctime(), keytab))
            else:
                realm = this_realm

    return realm

def krb5_get_zones():
    conn.conn = conn.iface()
    data = container.PAPIData(conn.conn, ['1', 'zones'], 'zones')

    # Try for up to 60 seconds to retrieve share information
    get_done, count = False, 0
    while not get_done and count < 20:      # 3 second waits
        try:
            zone_data = data.get()
            get_done = True
        except rest.RestError as e:
            logger.error("%s: Krb5 will retry to read zones: %s" %
                         (time.ctime(), str(e)))

        count += 1
        if not get_done:
            time.sleep(3)

    if not get_done:
        raise RuntimeError('%s: Krb5: could not fetch zone info' % time.ctime())
    return zone_data

# copied from chopu-auth-upgrade
def start_lsass():
    import isi.config.mcp as mcp
    mcpconfig = mcp.mcpconfig()
    mcpconfig.showall()
    mcpconfig.monitor('auth')
    mcpconfig.enable('auth')
    # give some time for mcp to react
    mcpconfig.runAction('lwsm', 'start-lwsmd')
    time.sleep(3)
    mcpconfig.runAction('auth', 'start-lsass')


def krb5_start_auth():
    conn.conn = conn.iface()
    data = container.PAPIData(conn.conn, ['1', 'auth', 'providers', 'summary'],
                              'provider_instances')

    # Try for up to 120 seconds to retrieve share information
    get_done, count = False, 0
    while not get_done and count < 40:      # 3 second waits
        try:
            auth_data = data.get()
            get_done = True
        except rest.RestError as e:
            logger.error("%s: Krb5 will retry to reach auth server: %s" %
                         (time.ctime(), str(e)))

        if not get_done:
            # don't constantly restart lsass, just occasionally
            if count % 10 == 0:
                start_lsass()
            else:
                time.sleep(3)
            count += 1


    if not get_done:
        raise RuntimeError('%s: Krb5: could not reach auth server' % time.ctime())
    return

def krb5_upgrade():
    zone_data = krb5_get_zones()
    UPGRADE_CMD = "/etc/mcp/override/krb5.conf.upgradecmd"
    run_cmd = False

    try:
        cmdfile = open(UPGRADE_CMD, 'w')
    except:
        raise RuntimeError('%s: KRB5 upgrade could not create %s' %
                           (time.ctime(), UPGRADE_CMD))

    # OK, we have to iterate over all the zones and look for the Kerberos
    # configuration in each. HDFS can have Kerb in any zone, but NFS can
    # only have it in the System zone

    for zone in zone_data:
        hdfs_keytab = None
        if 'hdfs_keytab' in zone:
            if 'hdfs_authentication' in zone:
                # by default, hdfs_keytab isn't empty, but instead points
                # to a file that doesn't exist, so check for that case
                if (zone['hdfs_authentication'] != 'simple_only') and \
                       (os.path.exists(zone['hdfs_keytab'])):
                    hdfs_keytab = zone['hdfs_keytab']
                    if hdfs_keytab[0:5] == 'FILE:': # strip FILE: prefix
                        hdfs_keytab = hdfs_keytab[5:]
                    elif hdfs_keytab[0:8] == 'DYNAMIC:':
                        hdfs_keytab = None # assume we reference zone's AD

        nfs_keytab = None
        if zone['id'] == 'System':
            try:
                infile = open(SYSCTL_CONF,'r')

                for line in infile.readlines():
                    if len(line) > 0 and line[-1] == '\n':
                        line = line[:-1]

                        fullline = line
                        try:
                            line = line[0:line.index('#')]
                        except ValueError:
                            pass

                        # all krb5 sysctls are in vfs.nfsrv
                        if not 'vfs.nfsrv' in line:
                            continue

                        try:
                            sysctl, value = line.split('=', 2)
                        except ValueError:
                            logger.warning("%s: NFS KRB5 upgrade found invalid sysctl: %s" %
                                           (time.ctime(), fullline))
                            continue

                        if len(value) > 0 and value[-1] == ' ':
                            value = value[:-1] # delete trailing space
                        if sysctl == "vfs.nfsrv.keytab" and \
                               (os.path.exists(value)):
                            nfs_keytab = value
            except:
                logger.warning("%s: NFS KRB5 upgrade unable to read sysctls" %
                               time.ctime())

        if nfs_keytab:
            if hdfs_keytab and hdfs_keytab != nfs_keytab:
                raise RuntimeError(("%s: Krb5: NFS and HDFS both have " + \
                                    "keytabs on same zone, cannot upgrade") %
                                   time.ctime())
            keytab = nfs_keytab
        else:
            keytab = hdfs_keytab
        if not keytab:
            continue

        realm = krb5_get_realm(keytab, logger)
        if not realm:
            logger.info("%s: Krb5 found no realm in keytab file %s" %
                        (time.ctime(), keytab))
            continue

        logger.info("%s: Krb5 config for realm %s based on keytab file %s" %
                    (time.ctime(), realm, keytab))

        cmd = "isi auth krb5 create " + realm + " --keytab-file=" + keytab
        cmdfile.write(cmd+"\n")

        cmd = "isi zone zones modify " + zone['id'] + " --add-auth-providers=" + \
          "lsa-krb5-provider:" + realm
        cmdfile.write(cmd+"\n")
        run_cmd = True

    cmdfile.close()

    if not run_cmd:
        logger.info("%s: No Krb5 config to migrate" % time.ctime())
        return

    try:
        os.chmod(UPGRADE_CMD, 0750)
    except:
        raise RuntimeError('%s: failed to make %s executable' %
                           (time.ctime(), UPGRADE_CMD))

    # the auth service may not have started up yet; wait for it
    krb5_start_auth()

    process = subprocess.Popen(UPGRADE_CMD, shell=True,
                               stdout=subprocess.PIPE,
                               stderr=subprocess.STDOUT)
    for line in process.stdout:
        logger.error("%s: upgrading: %s" % (time.ctime(), line))
    process.wait()
    if process.returncode == 0:
        logger.info("%s: Krb5 migrated successfully" % time.ctime())
    else:
        raise RuntimeError("%s: failed to migrate Kerberos settings" %
                               time.ctime())

    try:
        os.unlink(UPGRADE_CMD)
    except:
        logger.error('%s: Unable to delete %s' % (time.ctime(), UPGRADE_CMD))


def main():

    nstat = sysver.Node_Status()
    prevNodeVersion = nstat.get_previous_version()
    if prevNodeVersion is None:
        return 0

    need_upgrade = False
    if prevNodeVersion < sysver.MOBY_VERSION:
        need_upgrade = True

    if need_upgrade:
        try:
            logger.setLevel(logging.DEBUG)
            fh = logging.FileHandler(LOG_FILE, 'a')
            fh.setLevel(logging.DEBUG)
            logger.addHandler(fh)
        except EnvironmentError, e:
            syslog.syslog(syslog.LOG_NOTICE,
                          "Error during logging "
                          "in moby-auth-upgrade script, %s" % e)
            return 0
        logger.setLevel(logging.DEBUG)

        try:
            os.chmod(LOG_FILE, 0640)
        except OSError, e:
            # log chmod errors and continue
            logger.error("%s: Error chmod'ing %s (errno: %d)" %
                         (time.ctime(), LOG_FILE, e.errno))

        # We need to block all other nodes from continuing until we've finished
        # the upgrade
        try:
            sentry = safelockfile(AUTH_UPGRADE_FILE, 'w')
        except EnvironmentError, e:
            logger.error("%s: Error during locking in moby-auth-upgrade script, %s" %
                         (time.ctime(), e))
            return 0

        if (uapi.disk_enabled(uapi.MOBY_UNFS) and \
            os.path.exists(AUTH_UPGRADE_DONE_FILE)):
            logger.info("%s: Upgrade already complete on %s" %
                        (time.ctime(), gethostname()))                
        else:
            logger.info("%s: Beginning upgrade on %s" %
                    (time.ctime(), gethostname()))
            krb5_upgrade()
            if (uapi.disk_enabled(uapi.MOBY_UNFS)):
                logger.info("%s: Marking upgrade as successful on %s" %
                            (time.ctime(), gethostname()))
                c = open(AUTH_UPGRADE_DONE_FILE, "w")
                c.close()
            
        sentry.close()

    return 0

if __name__ == "__main__":
    try:
        sys.exit(main())
    except Exception, e:
        syslog.syslog(syslog.LOG_ERR,
                      "Error running moby-auth-upgrade script, %s" % e)
