#! /usr/bin/env python
'''A tool that assists with CTO field upgrades'''

# Copyright C 2008-2011 by Isilon Systems, Inc. All rights not expressly
# granted herein are reserved.
# Created in the United States of America.
# 
# This digital media is protected by U.S. and international copyright and
# intellectual property laws. Unless otherwise specified, all information
# and screens appearing as part of this digital medium, including
# software, services, documents, text, images, icons, and logos design;
# the selection, assembly, arrangement, and design thereof; and the code
# that enables its presentation, are the sole property of Isilon Systems,
# Inc.
# 
# Except as otherwise required by applicable law, no part of any
# copyrighted material may be reproduced, modified, translated,
# distributed, published, or transmitted by any means, electronic or
# mechanical, for any purpose, without the prior written consent of Isilon
# Systems. 
#  
# Isilon and Isilon Systems are registered trademarks of Isilon Systems,
# Inc.  Legion is a trademark of Isilon Systems, Inc.  All other
# trademarks and registered trademarks are the property of their
# respective holders.

#
# A tool that assists with CTO field upgrades
#
import os
import re
import sys
import time
import getopt
import syslog

import isi.error
import isi.app.lib.procs as procs
import isi.hw.hal as hal
import isi.misc

import isi_cto_lib as cto

#----------------------------------------------------------------------
ROLLBACK_RECEIPT = 'rollback_receipt.xml'

#----------------------------------------------------------------------
def unprovisionDrives():
    '''If Mavericks, unprovision all drives from the node so they get correctly
        re-provisioned for the new node type on reboot'''

    error = 0

    if isi.misc.osrevision() > 0x700000000000000:
        from isi.app.lib import safelockfile
        import isi.config.arrcfg as arrcfg
        import isi.smartpools.diskpools as spdp
        import isi.sys.version as sysver

        #XXX This needs to match isi_evaluate_provision_drive
        DRIVE_PROVISION_LOCK_FILE = "/ifs/.ifsvar/drive_provision.lock"

        print 'Unprovisioning drives, will get reprovisioned on reboot'
        try:
            lockfile = safelockfile(DRIVE_PROVISION_LOCK_FILE, "w")
        except IOError as e:
            print "Failed to obtain provisioning lock"
            error = 1
            return error

        updated_cache = False
        try:
            # XXX We need the node info cache to have the new type's info so
            # other nodes that might provision our drives for us will do the
            # right thing
            error = os.system('isi_inventory_tool --reInit')
            if error:
                print 'Unable to re-initialize HAL on this node'
                lockfile.close()
                return error

            error = hal.reInitialize()
            if error:
                print 'Unable to re-initialize HAL'
                lockfile.close()
                return error

            error = os.system('killall isi_group_change_d')
            if error:
                error = os.system('killall -SIGKILL isi_group_change_d')
                if error:
                    print 'Unable to kill isi_group_change_d'
                    lockfile.close()
                    return error

            updated_cache = sysver.update_cache()
        except:
            updated_cache = False

        if not updated_cache:
            print "Failed to update node info cache for provisioning"
            error = 1
            lockfile.close()
            return error

        MAX_TRIES = 5
        tries = 0
        while tries < MAX_TRIES:
            try:
                dpcfg = spdp.open_config()
                if not dpcfg.mav_upgraded():
                    print 'SmartPools not yet upgraded to Mavericks'
                    error = 1
                    lockfile.close()
                    return error
                aconf = arrcfg.config_load()
                local_lnn = aconf.local_lnn

                # Grab this for later
                local_devid = dpcfg._get_db().get_lnn_to_devid_map()[local_lnn]

                for pool in dpcfg.db.get_disk_pools():
                    # drive is (lnn, ldnum)
                    local_drives = [d for d in pool.drives if d[0] == local_lnn]
                    local_drives.sort(key=lambda (lnn, ldnum): ldnum)
                    if len(local_drives):
                        print "Unprovisioning", local_drives, "from", pool
                        pool.rem_drives(local_drives)

                dpcfg.db.write_db()
                break
            except:
                tries += 1
                print "Writing SmartPools DB failed, retrying %d times" % \
                        (MAX_TRIES - tries)

        if tries >= MAX_TRIES:
            print "Failed to write SmartPools database after %d retries" % \
                    MAX_TRIES
            error = 1
            lockfile.close()
            return error

        lockfile.close()


    if isi.misc.osrevision() > 0x701010000000000:
        # Next, we must remove this devid from the drive_purpose_db
        import isi.fs.drive_purposing as fsdp

        tries = 0
        while tries < MAX_TRIES:
            purposedb = fsdp.DrivePurposeDB()

            # This will distinguish between pre-moby and moby+ eq entries access
            eq_entries = purposedb.entries.get("equivalence_entries",
                    purposedb.entries)

            if not local_devid:
                print "Failed to determine devid"
                error = 1
                return error

            # find and remove this devid from the drive purpose db
            for eq_entry in eq_entries.values():
                for node_type in eq_entry["node_types"]:
                    devids = node_type[1]
                    if local_devid in devids:
                        print "Removing devid %d from the drive purpose db" % \
                                local_devid
                        devids.remove(local_devid)

            # make sure the db gets rid of empty entries in case we emptied one
            # by removing this devid
            purposedb._clean_empty()

            if purposedb.find_equivalence_entry_for_devid(local_devid):
                print "Failed to remove devid %d from the drive purpose db" % \
                        local_devid
                error = 1
                return error

            try:
                purposedb.write()
                break
            except (IOError, OSError), e:
                if (e.errno not in (errno.ESTALE, errno,ENOTDIR) or
                       tries >= MAX_TRIES):
                    raise
                print "Writing DrivePurpose DB failed, retrying %d times" % \
                        (MAX_TRIES - tries)

            tries += 1

        if tries >= MAX_TRIES:
            print "Failed to write DrivePurpose database after %d retries" % \
                    MAX_TRIES
            error = 1

    return error

#----------------------------------------------------------------------
def doPreInstallActions(packageLocation, noPrompt=False):
    '''Perform the pre-installation actions on the node. This includes
        verifying that the node is indeed CTO capable, the node's chassis
        VPD is consistent between disk and EEPROM, the upgrade is
        being installed on the correct node using the node's serial
        number as the authentication key, the node's configuration check
        results are acceptable to perform the upgrade, importing the
        new CTO files, re-Initializing HAL, unprovisioining drives from this
        node (so they get reprovisioned correctly as the new type on reboot)
        and running a second configuration check to insure the node is in an
        acceptable state to accept the upgrade kit. Returns error status
        (error). if 'noPrompt' is True, all user prompting will be skipped.
        This is intended solely for development and QA testing and should
        NEVER be used in the field.'''

    error = 0

    # Make sure the node is CTO capable
    error = cto.verifyCtoCapable()
    if error:
        return error

    print 'Node CTO capability verified'

    # Get the order no. and node serial no. for the upgrade
    (error, orderNo) = cto.readFile('%s' % cto.ORDERNO_NAME)
    if error:
        return error
    (error, serialNo) = cto.readFile('%s' % cto.SERIALNO_NAME)
    if error:
        return error

    print 'Upgrade is for Order %s, Node S/N %s' % (orderNo, serialNo)

    # Verify the chassis VPD
    (error, nodeSku) = cto.verifyVPD(cto.VPDTYPE_BASE_SKU)
    if error:
        return error
    (error, nodeSerialNo) = cto.verifyVPD(cto.VPDTYPE_SERIALNO)
    if error:
        return error

    print 'Chassis VPD verified'

    nodeSerialNoDashes = nodeSerialNo.replace('-', '')
    # Verify the upgrade is being applied to the correct node
    if nodeSerialNoDashes != serialNo:
        print 'You are attempting to install the upgrade on the wrong node'
        print ('Node S/N is %s, package is for %s' %
            (nodeSerialNoDashes, serialNo))
        print 'Upgrade terminated!'
        error = 1
        return error

    print 'Node S/N verified'

    # Run a configuration check before the upgrade is installed
    # to verify the state of the node is acceptable for the upgrade
    error = cto.verifyConfigCheck()
    if error:
        if not noPrompt:
            print ''
            print 'WARNING: Node appears to have some configuration issues'
            answer = cto.ask('Continue with upgrade anyway? (y/n) ')
            if answer.lower().startswith('y'):
                print 'Continuing with upgrade...'
            else:
                print 'Upgrade aborted due to configuration check errors'
                error = 1
                return error
        else:
            print 'Skipping user prompting due to noprompt option'
    else:
        print 'Node CTO configuration verified'

    # Save a rollback version of the receipt
    cmd = ('/usr/bin/isi_inventory_tool --display --itemType receipt > %s' %
        (os.path.join(packageLocation, ROLLBACK_RECEIPT)))
    (error, output) = cto.runCommand(cmd, needSudo = True)
    if error:
        return error

    # Import the new CTO files
    serialNoDashes = serialNo.replace('-', '')
    receiptName = '%s_%s.xml' % (orderNo, serialNoDashes)
    error = cto.importCtoInformation(packageLocation, receiptName)
    if error:
        return error

    print 'CTO files imported'

    # Kick HAL
    error = hal.reInitialize()
    if error:
        print 'Unable to re-initialize HAL'
        return error

    print 'HAL re-initialized'

    # If Mavericks, unprovision all drives from the node so they get correctly
    # re-provisioned for the new node type on reboot
    error = unprovisionDrives()
    if error:
        return error

    # Run a second configuration check. This one should fail because we
    # just give it a new receipt but have not installed the upgrade
    # kit as of yet
    error = cto.verifyConfigCheck()
    if error:
        if not noPrompt:
            print ''
            print 'Configuration check failed, which is expected.'
            answer = cto.ask('Do the failures look acceptable to continue '
                'with upgrade? (y/n) ')
            if answer.lower().startswith('y'):
                print 'Continuing with upgrade...'
            else:
                print 'Upgrade aborted due to configuration check errors'
                error = 1
                return error
        else:
            print 'Skipping user prompting due to noprompt option'
    else:
        if not noPrompt:
            print ''
            print ('WARNING: Configuration check passed, even though upgrade '
                'kit has yet to be installed')
            answer = cto.ask('Continue with upgrade anyway? (y/n) ')
            if answer.lower().startswith('y'):
                print 'Continuing with upgrade...'
            else:
                print ('Upgrade aborted due configuration check failing to '
                    'find errors')
                error = 1
                return error
        else:
            print 'Skipping user prompting due to noprompt option'

    # Ask user if node should be powered down so that the upgrade can
    # be installed
    if not noPrompt:
        print ''
        answer = cto.ask('Ready to power down the node? (y/n) ')
        if answer.lower().startswith('y'):
            print 'Powering down node'
        else:
            print 'Aborting power down by user request'
            error = 1
            return error
    else:
        print 'Skipping user prompting due to noprompt option'

    # Remind user to run post installation actions after upgrade is installed
    # and node is powered back up
    print ''
    print '               ***** ATTENTION *****'
    print 'Wait for node to power down, then install the upgrade kit.'
    print 'Once installation is complete, and the node is powered back on, '
    print 'do not forget to run the post-installation actions.'
    print '               ***** ATTENTION *****'
    error = cto.rebootNode(cto.METHOD_SHUTDOWN)
    if error:
        return error

    return error

#----------------------------------------------------------------------
def doPostInstallActions(noPrompt, noSendABR=False):
    '''Perform post-installation actions which include running a node
        configuration check to make sure the node is in an acceptable
        state to generate and send a new ABR back to Isilon. Returns
        error status (error). if 'noPrompt' is True, all user prompting
        will be skipped. This is intended solely for development and QA
        testing and should NEVER be used in the field. If 'noSendABR'
        is True, rather than sending the new ABR to Isilon, a copy of
        the new ABR will be left in the user's current directory.'''

    # Make sure the node is CTO capable
    error = cto.verifyCtoCapable()
    if error:
        return error

    print 'Node CTO capability verified'

    # Run a configuration check on the new configuration
    error = cto.verifyConfigCheck()
    if error:
        if not noPrompt:
            print ''
            answer = cto.ask('Generate a new ABR anyway? (y/n) ')
            if answer.lower().startswith('y'):
                print 'Generating new ABR and sending to Isilon'
            else:
                print ('Fix the failures and re-run post-installation actions')
                error = 1
                return error
        else:
            print 'Skipping user prompting due to noprompt option'

    print 'Chassis VPD verified'

    # Get the node's serial no.
    (error, serialNo) = cto.verifyVPD(cto.VPDTYPE_SERIALNO)
    if error:
        return error

    # Generate a new ABR and send it to Isilon
    error = cto.generateAndSendABR(serialNo, noSendABR)
    if error:
        return error

    if not noSendABR:
        print 'ABR generated and sent to Isilon'

    return error

#----------------------------------------------------------------------
def doOverview(packageLocation):
    '''Show a before/after receipt comparison.  Compare to the receipt
        currently on the node if the upgrade has not been done yet.
        If the upgrade has already been done, compare to the rollback
        receipt that was saved in packageLocation during the
        preinstallation actions'''

    # Make sure the node is CTO capable
    error = cto.verifyCtoCapable()
    if error:
        return error

    print 'Node CTO capability verified'

    # Get the order no. and node serial no. for the upgrade
    (error, orderNo) = cto.readFile('%s' % cto.ORDERNO_NAME)
    if error:
        return error
    (error, serialNo) = cto.readFile('%s' % cto.SERIALNO_NAME)
    if error:
        return error

    # Verify the chassis VPD
    (error, nodeSku) = cto.verifyVPD(cto.VPDTYPE_BASE_SKU)
    if error:
        return error
    (error, nodeSerialNo) = cto.verifyVPD(cto.VPDTYPE_SERIALNO)
    if error:
        return error

    print 'Chassis VPD verified'

    nodeSerialNoDashes = nodeSerialNo.replace('-', '')
    # Verify the upgrade package belongs to this node
    if nodeSerialNoDashes != serialNo:
        print ('Node S/N is %s, package is for %s' %
            (nodeSerialNoDashes, serialNo))
        error = 1
        return error

    print 'Node S/N verified'

    # Run a configuration check
    error = cto.verifyConfigCheck()
    if error:
        print 'WARNING: Node appears to have some configuration issues'
    else:
        print 'Node CTO configuration verified'

    # Show receipt before upgrade (from rollback version if necessary)
    print 'Receipt before upgrade:\n'
    receiptPath = os.path.join(packageLocation, ROLLBACK_RECEIPT)
    if os.path.isfile(receiptPath):
        (error, receiptText) = cto.readFile('%s' % receiptPath)
        if error:
            return error
        print receiptText
    else:
        cmd = '/usr/bin/isi_inventory_tool --display --itemType receipt'
        (error, receiptText) = cto.runCommand(cmd, needSudo = True)
        if error:
            return error
        print '%s\n' % '\n'.join(receiptText)

    # Show receipt after upgrade (from package)
    serialNoDashes = serialNo.replace('-', '')
    receiptName = '%s_%s.xml' % (orderNo, serialNoDashes)
    receiptPath = os.path.join(packageLocation, receiptName)
    (error, receiptText) = cto.readFile('%s' % receiptPath)
    if error:
        return error
    print 'Receipt after upgrade:\n'
    print receiptText

#----------------------------------------------------------------------
def doRollbackActions(packageLocation):
    '''Rollback to the receipt that was on the node before the upgrade.
        The rollback receipt is saved in packageLocation during the
        pre-installation actions so this does not work after the
        upgrade package is uninstalled'''

    error = 0

    # Make sure the node is CTO capable
    error = cto.verifyCtoCapable()
    if error:
        return error

    print 'Node CTO capability verified'

    # Get the order no. and node serial no. for the upgrade
    (error, orderNo) = cto.readFile('%s' % cto.ORDERNO_NAME)
    if error:
        return error
    (error, serialNo) = cto.readFile('%s' % cto.SERIALNO_NAME)
    if error:
        return error

    # Verify the chassis VPD
    (error, nodeSku) = cto.verifyVPD(cto.VPDTYPE_BASE_SKU)
    if error:
        return error
    (error, nodeSerialNo) = cto.verifyVPD(cto.VPDTYPE_SERIALNO)
    if error:
        return error

    print 'Chassis VPD verified'

    nodeSerialNoDashes = nodeSerialNo.replace('-', '')
    # Verify the rollback is being attempted on the correct node
    if nodeSerialNoDashes != serialNo:
        print 'You are attempting to rollback the upgrade on the wrong node'
        print ('Node S/N is %s, package is for %s' %
            (nodeSerialNoDashes, serialNo))
        print 'Upgrade terminated!'
        error = 1
        return error

    print 'Node S/N verified'

    # Import rollback receipt
    receiptPath = os.path.join(packageLocation, ROLLBACK_RECEIPT)
    if os.path.isfile(receiptPath):
        print 'Importing receipt (%s)' % receiptPath
        cmd = ('/usr/bin/isi_inventory_tool --importFile --fileType receipt '
            '--filePath %s' % receiptPath)
        (error, output) = cto.runCommand(cmd, needSudo = True)
        if error:
            print 'Unable to import receipt'
            return error
        else:
            print 'Success'
    else:
        print 'Rollback receipt does not exist'
        error = 1
        return error

    print 'Rollback receipt imported'

    # Kick HAL
    error = hal.reInitialize()
    if error:
        print 'Unable to re-initialize HAL'
        return error

    print 'HAL re-initialized'

    # If Mavericks, unprovision all drives from the node so they get correctly
    # re-provisioned for the new node type on reboot
    error = unprovisionDrives()
    if error:
        return error

    # Run a configuration check
    error = cto.verifyConfigCheck()
    if error:
        print 'WARNING: Node appears to have some configuration issues'
    else:
        print 'Node CTO configuration verified'

    # Ask user if node should be powered down so that the rollback can
    # be completed
    print ''
    answer = cto.ask('Ready to power down the node? (y/n) ')
    if answer.lower().startswith('y'):
        print 'Powering down node'
    else:
        print 'Aborting power down by user request'
        error = 1
        return error

    # Remind user to run post installation actions after upgrade is installed
    # and node is powered back up
    print ''
    print '               ***** ATTENTION *****'
    print 'Wait for node to power down, then un-install the upgrade kit.'
    print 'Once un-installation is complete, and the node is powered back '
    print 'on, do not forget to run the post-installation actions.'
    print '               ***** ATTENTION *****'
    error = cto.rebootNode(cto.METHOD_SHUTDOWN)
    if error:
        return error

    return error

#----------------------------------------------------------------------
def usage():
    '''Print the usage message'''

    usage = (
'''Usage: %s [OPTION] [ARGS]

OPTIONS:
    -h, --help  Prints this text
    --filepath <package location>  Designate a package location
    --pre   Perform pre-installation actions
    --post  Perform post-installation actions
    --overview  Compare receipt before and after upgrade
    --rollback  Undo upgrade
''')

    print usage % sys.argv[0]

#----------------------------------------------------------------------
def main(argv):
    '''The main field upgrade script'''

    productSku = None
    SerialNo = None
    preActions = False
    postActions = False
    noPrompt = False
    noSendABR = False
    filepath = None
    overview = False
    rollback = False

    try:
        # NOTE: The 'noprompt' and 'nosendabr' options are for testing
        #       only and hidden from the usage message on purpose.
        opt, args = getopt.getopt(argv, 'h', ['pre', 'post', 'noprompt',
            'nosendabr', 'filepath=', 'overview', 'rollback'])

    except getopt.GetoptError, err:
        print str(err)
        usage()
        sys.exit(1)

    for o, a in opt:
        if o in ('-h', '--help'):
            usage()
            sys.exit(0)
        elif o in ('--pre'):
            preActions = True
        elif o in ('--post'):
            postActions = True
        elif o in ('--noprompt'):
            noPrompt = True
        elif o in ('--nosendabr'):
            noSendABR = True
        elif o in ('--filepath'):
            filepath = a
        elif o in ('--overview'):
            overview = True
        elif o in ('--rollback'):
            rollback = True

    if not (preActions or postActions or overview or rollback):
        print 'Missing required option'
        usage()
        sys.exit(1)

    if sum(1 if x else 0 for x in (preActions, postActions,
            overview, rollback)) > 1:
        print 'Must be run separately'
        usage()
        sys.exit(1)

    # Filepath option must be provided in compliance mode
    if isi.misc.sysctlbyname('isi.compliance') == 1 and not filepath:
        print '--filepath argument must be used in compliance mode'
        usage()
        sys.exit(1)

    syslog.openlog('isi_cto_upgrade', 0, syslog.LOG_USER)

    (packageLocation, scriptName) = cto.getPackageInfo(sys.argv[0])
    # If --filepath option is used, override packageLocation
    if filepath:
        packageLocation = filepath

    msg = 'Verifying package integrity'
    syslog.syslog(syslog.LOG_NOTICE, msg)
    print msg
    error = cto.verifyMd5(packageLocation)
    if error:
        msg = 'Package integrity is BAD'
        syslog.syslog(syslog.LOG_CRIT, msg)
        print msg
        sys.exit(1)
    msg = 'Package integrity is good'
    syslog.syslog(syslog.LOG_NOTICE, msg)
    print msg

    if preActions:
        msg = 'Beginning pre-installation actions'
        syslog.syslog(syslog.LOG_NOTICE, msg)
        print msg
        error = doPreInstallActions(packageLocation, noPrompt)
        if error:
            msg = 'Pre-installation actions failed'
            syslog.syslog(syslog.LOG_CRIT, msg)
            print msg
            sys.exit(1)
        msg = 'Pre-installation actions successful'
        syslog.syslog(syslog.LOG_NOTICE, msg)
        print msg
    elif postActions:
        msg = 'Beginning post-installation actions'
        syslog.syslog(syslog.LOG_NOTICE, msg)
        error = doPostInstallActions(noPrompt, noSendABR)
        if error:
            msg = 'Post-installation actions failed'
            syslog.syslog(syslog.LOG_CRIT, msg)
            print msg
            sys.exit(1)
        msg = 'Post-installation actions successful'
        syslog.syslog(syslog.LOG_NOTICE, msg)
        print msg
    elif overview:
        error = doOverview(packageLocation)
        if error:
            print "Receipt comparison failed"
            sys.exit(1)
    elif rollback:
        msg = 'Beginning rollback actions'
        syslog.syslog(syslog.LOG_NOTICE, msg)
        error = doRollbackActions(packageLocation)
        if error:
            msg = 'Rollback actions failed'
            syslog.syslog(syslog.LOG_CRIT, msg)
            print msg
            sys.exit(1)
        msg = 'Rollback actions successful'
        syslog.syslog(syslog.LOG_NOTICE, msg)
        print msg

    print 'Done'
    sys.exit(0)

#----------------------------------------------------------------------
if __name__ == '__main__':
    main(sys.argv[1:])
