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

import logging
import os
import subprocess
import sys
import syslog
import getopt

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

import isi.config as iconf
import isi.gconfig as gcfg
import isi.app.lib.upgrade_api as uapi
from isi.app.lib import safelockfile
from isi.app.lib.confParser import ConfParser

NFS_DEFAULT_PATH = "/ifs/.ifsvar/modules/nfs"
LOG_FILE = os.path.join(NFS_DEFAULT_PATH, "moby-nfs-upgrade.log")
NFS_UPGRADE_LOCKFILE = os.path.join(NFS_DEFAULT_PATH, "moby-nfs-upgraded")
NFS_UPGRADED_FILE = os.path.join(NFS_DEFAULT_PATH, "moby-nfs-upgrade-complete")
NFS_GCONFIG_FILE = os.path.join(NFS_DEFAULT_PATH, "nfs_config.gc")

# sysctl.conf defines
SYSCTL_CONF = "/etc/mcp/override/sysctl.conf"
SYSCTL_WATCH = '/etc/mcp/sys/files/nfs-moby-config-upgrade'
EXPORTS_WATCH = '/etc/mcp/sys/files/nfs-moby-exports-upgrade'
MAX_COMMIT_RETRIES = 100

logger = logging.getLogger("moby_nfs_upgrade")

LWREG_PREFIX = 'registry.Services.lwio.Parameters.Drivers.nfs.'
SYSCTL_PREFIX = 'sysctls.entries.'
ZONE_SETTINGS_PREFIX = 'zone_list.zones.1.settings.'

NFS_GCFG_CONVERSION_DICT = {
    "vfs.nfsrv.nfsv4.domainname":"domain",
    "vfs.nfsrv.nfsv4.idmap_nonames":"no_names",
    "vfs.nfsrv.nfsv4.idmap_replacedomain":"replace_domain",
    "vfs.nfsrv.nfsv4.idmap_nodomain_uids":"no_domain_uids",
    "vfs.nfsrv.nfsv4.idmap_nodomain":"no_domain",
}

NFS_LWREG_CONVERSION_DICT = {
    "vfs.nfs.downdelayinitial":"DownDelayInitial",
    "vfs.nfs.downdelayinterval":"DownDelayInterval",
    "vfs.nfs.export_config_backup_copies":"ExportConfigBackupCopies",
    "vfs.nfs.keytab_enctype":"KeytabEncType",
    "vfs.nfs.skip_wcc_data_onerr":"SkipWCCDataOnErr",
    "vfs.nfs.nfs3_jukebox_delay":"NFS3JukeboxDelay",
    "vfs.nfs.reconnects":"Reconnects",
    "vfs.nfs.bufpackets":"BufPackets",
    "vfs.nfs.callback_addr":"CallbackAddr",
    "vfs.nfs.realign_count":"RealignCount",
    "vfs.nfs.realign_test":"RealignTest",
    "vfs.nfs.defect":"Defect",
    "vfs.nfs.iodmax":"IodMax",
    "vfs.nfs.iodmin":"IodMin",
    "vfs.nfs.iodmaxidle":"IodMaxIdle",
    "vfs.nfs.diskless_rootaddr":"DisklessRootAddr",
    "vfs.nfs.diskless_rootpath":"DisklessRootpath",
    "vfs.nfs.diskless_valid":"DisklessValid",
    "vfs.nfs.nfs_ip_paranoia":"NfsIpParanoia",
    "vfs.nfs.nfs_directio_allow_mmap":"NfsDirectioAllowMmap",
    "vfs.nfs.nfs_directio_enable":"NfsDirectioEnable",
    "vfs.nfs.clean_pages_on_close":"CleanPagesOnClose",
    "vfs.nfs.commit_on_close":"CommitOnClose",
    "vfs.nfs.prime_access_cache":"PrimeAccessCache",
    "vfs.nfs.access_cache_timeout":"AccessCacheTimeout",
    "vfs.nfs.debug":"Debug",
    "vfs.nfs.cache_floodlevel":"CacheFloodLevel",
    "vfs.nfsrv.nfsv4.callback_timeout":"NFSV4CallbackTimeout",
    "vfs.nfsrv.nfsv4.upcall_timeout":"NFSV4UpcallTimeout",
    "vfs.nfsrv.nfsv4.lease_time":"NFSV4LeaseTime",
    "vfs.nfsrv.nfsv4.acls_return_synthetic":"NFSV4AclsReturnSynthetic",
    "vfs.nfsrv.nfsv4.groupat_identifier":"NFSV4GroupAtIdentifier",
    "vfs.nfsrv.nfsv4.use_acls":"NFSV4UseAcls",
    "vfs.nfsrv.nfsv4.defaultgid":"NFSV4DefaultGID",
    "vfs.nfsrv.nfsv4.defaultuid":"NFSV4DefaultUID",
    "vfs.nfsrv.nfsv4.idmap_alert_threshold_duration":"AlertFailedIDQueryThrottleDurationSec",
    "vfs.nfsrv.nfsv4.idmap_alert_threshold_count":"AlertFailedIDQueryThrottleThreshold",
    "vfs.nfsrv.nfsv4.nfs4_returnoldstateid":"NFSV4Nfs4ReturnOldStateID",
    "vfs.nfsrv.nfsv4.max_badseqid":"NFSV4MaxBadSeqID",
    "vfs.nfsrv.nfsv4.lease_delta":"NFSV4LeaseDelta",
    "vfs.nfsrv.nfsv4.servertimer_upperbound_exceeded":"NFSV4ServerTimerUpperboundExceeded",
    "vfs.nfsrv.nfsv4.servertimer_upperbound":"NFSV4ServerTimerUpperbound",
    "vfs.nfsrv.nfsv4.openowner_noopen_expiry":"NFSV4OpenOwnerNoopenExpiry",
    "vfs.nfsrv.nfsv4.lockowner_nolock_expiry":"NFSV4LockOwnerNolockExpiry",
    "vfs.nfsrv.nfsv4.expire_on_shutdown":"NFSV4ExpireOnShutdown",
    "vfs.nfsrv.nfsv4.expire_on_disconnect":"NFSV4ExpireOnDisconnect",
    "vfs.nfsrv.nfsv4.access_write_allows_read":"NFSV4AccessWriteAllowsRead",
    "vfs.nfsrv.nfsv4.specialreads_bypass_locks":"NFSV4SpecialReadsBypassLocks",
    "vfs.nfsrv.nfsv4.honor_deny_delete":"NFSV4HonorDenyDelete",
    "vfs.nfsrv.nfsv4.check_utf8":"NFSV4CheckUTF8",
    "vfs.nfsrv.nfsv4.zero_offset_verifier_allowed":"NFSV4ZeroOffsetVerifierAllowed",
    "vfs.nfsrv.nfsv4.allow_owner_override":"NFSV4AllowOwnerOverride",
    "vfs.nfsrv.nfsv4.enable_locallocks":"NFSV4EnableLocallocks",
    "vfs.nfsrv.fake_dir_delete_access":"FakeDirDeleteAccess",
    "vfs.nfsrv.use_local_nfsver":"UseLocalNFSVer",
    "vfs.nfsrv.cache_udp_threshold":"CacheUDPThreshold",
    "vfs.nfsrv.cache_tcp_threshold":"CacheTCPThreshold",
    "vfs.nfsrv.cache_reconnect_timeout":"CacheReconnectTimeout",
    "vfs.nfsrv.cache_check_interval":"CacheCheckInterval",
    "vfs.nfsrv.cache_tcptimeout":"CacheTCPTimeout",
    "vfs.nfsrv.cache_udphighwater":"CacheUDPHighwater",
    "vfs.nfsrv.cache_udptimeout":"CacheUDPTimeout",
    "vfs.nfsrv.cache_enabled":"CacheEnabled",
    "vfs.nfsrv.server_v4enabled":"ServerV4Enabled",
    "vfs.nfsrv.server_v3enabled":"ServerV3Enabled",
    "vfs.nfsrv.nfs_privport":"NFSPrivPort",
    "vfs.nfsrv.errors_clear":"ErrorsClear",
    "vfs.nfsrv.errors_v4":"ErrorsV4",
    "vfs.nfsrv.errors_v3":"ErrorsV3",
    "vfs.nfsrv.log_errmap_defaults":"LogErrmapDefaults",
    "vfs.nfsrv.request_space_low":"RequestSpaceLow",
    "vfs.nfsrv.request_space_high":"RequestSpaceHigh",
    "vfs.nfsrv.max_wsize":"MaxWSize",
    "vfs.nfsrv.max_rsize":"MaxRSize",
    "vfs.nfsrv.create_attributes_ids_enabled":"CreateAttributesIdsEnabled",
    "vfs.nfsrv.create_attributes_enabled":"CreateAttributesEnabled",
    "vfs.nfsrv.readdir_requires_read":"ReaddirRequiresRead",
    "vfs.nfsrv.dopanic":"DoPanic",
    "vfs.nfsrv.allow_kerberos_squashing":"AllowKerberosSquashing",
    "vfs.nfsrv.commit_miss":"CommitMiss",
    "vfs.nfsrv.commit_blks":"CommitBlks",
    "vfs.nfsrv.mirrormnt":"MirrorMnt",
    "vfs.nfsrv.fha.bin_shift":"FhaBinShift",
    "vfs.nfsrv.fha.multiwriter_enabled":"FhaMultiwriterEnabled",
    "vfs.nfsrv.fha.max_nfsds_per_fh":"FhaMaxNfsdsPerFh",
    "vfs.nfsrv.fha.shared_max_per_fh":"FhaSharedMaxPerFh",
    "vfs.nfsrv.fha.exclusive_max_per_fh":"FhaExclusiveMaxPerFh",
    "vfs.nfsrv.fha.activity":"FhaActivity",
    "vfs.nfsrv.fha.stats":"FhaStats",
    "vfs.nfsrv.fha.stats_clear":"FhaStatsClear",
    "vfs.nfsrv.rpc.threads_min":"RpcThreadsMin",
    "vfs.nfsrv.rpc.threads_max":"RpcThreadsMax",
    "vfs.nfsrv.rpc.threads_alloc_current":"RpcThreadsAllocCurrent",
    "vfs.nfsrv.rpc.threads_alloc_highwater":"RpcThreadsAllocHighwater",
    "vfs.nfsrv.rpc.request_space_used":"RpcRequestSpaceUsed",
    "vfs.nfsrv.rpc.request_space_used_highest":"RpcRequestSpaceUsedHighest",
    "vfs.nfsrv.rpc.request_space_high":"RpcRequestSpaceHigh",
    "vfs.nfsrv.rpc.request_space_overcommit":"RpcRequestSpaceOvercommit",
    "vfs.nfsrv.rpc.request_space_low":"RpcRequestSpaceLow",
    "vfs.nfsrv.rpc.request_space_throttled":"RpcRequestSpaceThrottled",
    "vfs.nfsrv.rpc.request_space_throttle_count":"RpcRequestSpaceThrottleCount",
    "vfs.nfsrv.rpc.overcommit_active":"RpcOvercommitActive",
    "vfs.nfsrv.rpc.overcommit_events":"RpcOvercommitEvents",
    "vfs.nfsrv.rpc.overcommit_off":"RpcOvercommitOff",
    "vfs.nfsrv.rpc.overcommit_on":"RpcOvercommitOn",
    "vfs.nfsrv.rpc.idletimeout":"RpcIdletimeout",
    "vfs.nfsrv.rpc.shutdown_state":"RpcShutdownState",
    "vfs.nfsrv.rpc.stats":"RpcStats",
    "vfs.nfsrv.rpc.disc_ifdown":"RpcDiscIfdown",
    "vfs.nfsrv.rpc.disc_addrgone":"RpcDiscAddrgone"
}

def upgrade_gcfg_tree_version():
    ctx = gcfg.open(gcfg.gcfg_nfs_config).ctx_new()
    ctx.read_create(path=':2')
    ctx.commit()

def add_config(main_cfg, nfs_cfg, name, value):
    if name in NFS_LWREG_CONVERSION_DICT.keys():
        try:
            value = long(value)
        except ValueError:
            pass
        try:
            main_cfg.write(LWREG_PREFIX+NFS_LWREG_CONVERSION_DICT[name], value)
        except TypeError:
            logger.error("Failed to migrate setting %s." % name)
        else:
            logger.info("Migrated setting %s." % name)

    elif name in NFS_GCFG_CONVERSION_DICT.keys():
        try:
            value = long(value)
        except ValueError:
            pass
        try:
            nfs_cfg.write(ZONE_SETTINGS_PREFIX+NFS_GCFG_CONVERSION_DICT[name], value)
        except TypeError:
            try:
                # Retry as bool
                nfs_cfg.write(ZONE_SETTINGS_PREFIX+NFS_GCFG_CONVERSION_DICT[name], value != 0)
            except TypeError:
                logger.error("Failed to migrate setting %s." % name)
        else:
            logger.info("Migrated setting %s." % name)


def sysctl_upgrade_conf(main_cfg, nfs_cfg):
    conf = ConfParser(SYSCTL_CONF)

    for sysctl in conf.values.iteritems():
        name = sysctl[0]
        value = sysctl[1]

        add_config(main_cfg, nfs_cfg, name, value)

    logger.info("Completed settings migration (1/2)")


def sysctl_upgrade_gc(main_cfg, nfs_cfg):
    old_sysctls = main_cfg.read(SYSCTL_PREFIX)

    for sysctl in old_sysctls:
        name = sysctl.get('name', '')
        value = sysctl.get('value', '')

        add_config(main_cfg, nfs_cfg, name, value)

    logger.info("Completed settings migration (2/2)")

def get_gcfg_ctx(tree):
    return gcfg.open(tree).ctx_new()

# Migrate sysctl.conf and gconfig sysctl tree to registry
def sysctl_upgrade():
    upgrade_gcfg_tree_version()

    main_cfg = get_gcfg_ctx(gcfg.gcfg_main)
    nfs_cfg = get_gcfg_ctx(gcfg.gcfg_nfs_config)

    for x in xrange(MAX_COMMIT_RETRIES):
        try:
            sysctl_upgrade_conf(main_cfg, nfs_cfg)
            sysctl_upgrade_gc(main_cfg, nfs_cfg)
            main_cfg.commit()
            nfs_cfg.commit()
            break
        except gcfg.RecoverableCommitError:
            logger.info("Retrying sysctl upgrade commit (%d)." % x)
    # Ran out of retries
    else:
        raise RuntimeError("NFS upgrade could not commit NFS gconfig")


def exports_upgrade():
    logger.info("Export conversion started. "
                "See /var/log/nfs_convert.log for more details.")
    process = subprocess.Popen('isi_nfs_convert > /dev/null 2>&1', shell=True)
    process.wait()
    if process.returncode == 0:
        logger.info("Successfully converted NFS exports")
    else:
        raise RuntimeError("Failed to convert NFS exports")


def get_options():
    default_options = {'--incremental'    : False,
                       '--update-config'  : True,
                       '--update-exports' : True,
                       '--force'          : False,
                       '--cleanup'        : False}

    try:
        opts = getopt.getopt(sys.argv[1:], '',
                             ['incremental',
                              'rolling',
                              'update-config',
                              'update-exports',
                              'force',
                              'cleanup'])
        if (len(opts) and len(opts[0])):
            # If any options provided, all options default to false unless set
            options = {'--incremental'    : False,
                       '--update-config'  : False,
                       '--update-exports' : False,
                       '--force'          : False,
                       '--cleanup'        : False}
            for opt in opts[0]:
                if opt[0] in options.keys():
                    options[opt[0]] = True
        else:
            options = default_options
    except Exception, e:
        logger.info("Couldn't determine arguments (%s), running with defaults" %
                    str(e))
        options = default_options

    # We should never get a cleanup call before running successfully at
    # least once. If this happens, run the upgrade to be safe.
    if (options.get('--cleanup', False)
        and not os.path.exists(NFS_UPGRADED_FILE)
        and not options.get('--force', False)):
        logger.warning("Received unexpected cleanup request, running upgrade "
                       "before cleaning.")
        options = default_options
        options['--cleanup'] = True

    return options


# Only drop our success file if this was the final script run
def should_drop_file(options):
    # Always drop on simultaneous upgrades
    if not options.get('--incremental', False):
        return True
    # There is a rolling upgrade still in progress, so we may need to update
    else:
        return False


def run_upgrade(options):
    success = True
    upgrade_funcs = []

    if options.get('--update-config', True):
        upgrade_funcs.append(sysctl_upgrade)
    if options.get('--update-exports', True):
        upgrade_funcs.append(exports_upgrade)

    for upgrade_func in upgrade_funcs:
        try:
            logger.info("Running upgrade routine: %s" % upgrade_func.func_name)
            upgrade_func()
        except RuntimeError, e:
            msg = ("Error running nfs upgrade routine %s: %s" %
                   (upgrade_func.func_name, str(e)))
            logger.error(msg)
            syslog.syslog(syslog.LOG_NOTICE, msg)
            print str(e)
            success = False
        else:
            logger.info("Finished upgrade routine: %s" % upgrade_func.func_name)


    if success:
        msg = "Upgrade was successful"
        if should_drop_file(options):
            print >> file(NFS_UPGRADED_FILE, 'w'), msg
            os.chmod(NFS_UPGRADED_FILE, 0640)
            remove_sysctl_watch()
            remove_exports_watch()

    else:
        msg = "Upgrade was unsuccessful"
    logger.info(msg)


def remove_sysctl_watch():
    try:
        os.remove(SYSCTL_WATCH)
        logger.info("Removed nfs sysctl watch %s" % SYSCTL_WATCH)
    except OSError, err:
        logger.error("Unable to delete nfs sysctl watch %s (%s)" % (SYSCTL_WATCH, str(err)))


def remove_exports_watch():
    try:
        os.remove(EXPORTS_WATCH)
        logger.info("Removed nfs exports watch %s" % EXPORTS_WATCH)
    except OSError, err:
        logger.error("Unable to delete nfs exports watch %s (%s)" % (EXPORTS_WATCH, str(err)))


def main():
    success = False
    wait_sentries = {}

    try:
        lnn = "Node %d -" % iconf.getNode()['node id']
        lfmt = lnn + "%(asctime)s - %(levelname)s - %(message)s"
        logger.setLevel(logging.INFO)
        fh = logging.FileHandler(LOG_FILE, 'a')
        fh.setLevel(logging.INFO)
        fh.setFormatter(logging.Formatter(lfmt))
        logger.addHandler(fh)

    except EnvironmentError, e:
        syslog.syslog(syslog.LOG_NOTICE,
                      "Error running nfs upgrade script: %s" % e)
        return 1

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

    logger.info("NFS upgrade started.")
    options = get_options()

    if options.get('--cleanup', False):
        remove_sysctl_watch()
        remove_exports_watch()
        return 0

    if options.get('--incremental', False):
        # Already done with the upgrade, just bail out
        if (uapi.disk_enabled(uapi.MOBY_UNFS)
            and not options.get('--force', False)):
            logger.info("NFS upgrade has already occurred successfully.")
            return 0

        # First, try to get the waiter lock for each relevant portion to be
        # upgraded. If someone is already in line don't bother, as we can't do
        # anything more than they will after they get a turn
        for feature in ['config', 'exports']:
            lockpath = os.path.join(NFS_DEFAULT_PATH,
                                    'moby-nfs-upgraded-%s.wait' % feature)

            if options.get('--update-' + feature, False):
                try:
                    wait_sentries[feature] = safelockfile(lockpath, 'w',
                                                          blocking=False)
                except IOError, e:
                    if (e.errno == os.errno.EACCES or
                        e.errno == os.errno.EAGAIN):
                        # Already taken care of, don't bother
                        logger.info("Updating %s already in progress" % feature)
                        options['--update-' + feature] = False
                    else:
                        logger.error('Error while acquiring waiter lock for'
                                     '%s: %s' % (feature, e))
                        return 1

        if len(wait_sentries) == 0:
            logger.info("No work to be done")
            return 0

    # Block on the worker lock
    try:
        sentry = safelockfile(NFS_UPGRADE_LOCKFILE, 'w')
    except EnvironmentError, e:
        syslog.syslog(syslog.LOG_NOTICE,
                      "Error running nfs upgrade script: %s" % e)
        for wait_sentry in wait_sentries.values():
            wait_sentry.close()
        if sentry:
            sentry.close()
        return 1

    # Check if we're already done
    if (not options.get('--incremental', False)
        and os.path.exists(NFS_UPGRADED_FILE)
        and not options.get('--force', False)):
        logger.info("NFS upgrade has already occurred successfully.")
        remove_sysctl_watch()
        remove_exports_watch()
        for wait_sentry in wait_sentries.values():
            wait_sentry.close()
        if sentry:
            sentry.close()
        return 0

    for wait_sentry in wait_sentries.values():
        wait_sentry.close()

    success = run_upgrade(options)

    if sentry:
        sentry.close()

    if success:
        return 0
    else:
        return 1


if __name__ == "__main__":
    sys.exit(main())
