#!/usr/local/bin/python

# Copyright C 2001-2011 by Isilon Systems LLC. 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.

### Isilon configure dons
#
# This script configures disk only accelerator nodes (DONS)
# In the future, this functionality should be automatically handled
# by MCP but for now we do it this way.

import os
import sys
import signal
from xml.dom.minidom import parse

import isi.hw.bay as bay
import isi.hw.hal as hal
import isi.hw.version as hwver
import isi.misc

import isi.ui.lcd.screens as screens
import isi.ui.lcd.library as lcd

def handle_lcd(msg1, msg2 = None):
    try:
        screen = screens.ListInputScreen(msg1, ['yes', 'no'])
   
        screen.CAN_CANCEL = False
        screen.CAN_EXIT = False
        result = lcd.show(screen)
        if result == 'yes':
            if msg2:
                return handle_lcd(msg2)
            else:
                return 1
        else:
            return 0
    except:
        return 2

def handle_console(msg1, msg2 = None):
    done = 0
    val = ''
    reset = 0
    print msg1
    while not done:
        try:
            val = raw_input(" (yes or no)?")
        except KeyboardInterrupt:
            return 2
        except EOFError:
            return 0
        if val not in ['yes', 'YES', 'y', 'no', 'NO', 'n']:
            print ""
            print "Please enter one of 'yes' or 'no'"
            print ""
            val = ''
        if val in ['yes', 'YES', 'y']:
            if msg2:
                reset = handle_console(msg2)
            else:
                reset = 1
            done = 1
        elif val in ['no','NO', 'n']:
            done = 1
       
    return reset

def kill_other(pid):
    os.kill(pid, signal.SIGKILL)
        
def prompt_user_for_reset_don(old_owner_serialno, owner_serialno, serialno):
    reset = 0
    pid_lcd = -1
    pid_console = -1
    pid_console = os.fork()
    msg1 = """
Storage expansion node %s was previously attached to platform node %s.
To attach storage expansion node %s to platform node %s, the storage expansion
node needs to be reset and its drives need to be reformatted.
Note! Reformatting the drives on the storage expansion node will destroy all
data currently on those drives.
Do you want to reset and attach storage expansion node %s
    """.strip() % (serialno, old_owner_serialno, serialno, owner_serialno, serialno) 

    msg2 = """
    This will reformat all drives and destroy all data currently on those drives.
    Are you sure you want to continue?
    """.strip()
    
    if pid_console == 0:
        sys.exit(handle_console(msg1, msg2))
    else:
        pid_lcd = os.fork()
        if pid_lcd == 0:
            #Close fds
            for i in range(0,3):
                os.close(i)
                sys.stdout = open('/dev/null', 'w')
                sys.stdin = open('/dev/null', 'r')
                sys.stderr = open('/dev/null', 'w')
            sys.exit(handle_lcd(msg1, msg2))
        else:
            if pid_lcd and pid_console:
                # wait for any child
                pid, status = os.wait()
                if os.WIFEXITED(status) and os.WEXITSTATUS(status) < 2:
                    reset = os.WEXITSTATUS(status)
                    if pid == pid_lcd:
                        kill_other(pid_console)
                    else:
                        kill_other(pid_lcd)
                else:
                    # wait for remaining child
                    pid, status = os.wait()
                    if os.WIFEXITED(status) and os.WEXITSTATUS(status) < 2:
                        reset = os.WEXITSTATUS(status)
                    else:
                        reset = 2
                return reset
            else: # we should never reach this case 
                sys.exit(2)
    
def is_don_ignore(serialno):
    ignore = False
    doc = parse(hwver.HARDWARE_CONFIG_FILE)
    dons = doc.documentElement.getElementsByTagName(hwver.DON_SERIALNO)

    for don in dons:
        if don.firstChild.nodeValue == serialno:
            ignore = True
            break
    doc.unlink()
    return ignore
    
    
def set_don_ignore(serialno):
    ignored = False
    doc = parse(hwver.HARDWARE_CONFIG_FILE)
    root = doc.documentElement
    donroot = root.getElementsByTagName(hwver.DON_IGNORE)
    if donroot:
        donroot = donroot[0]
        dons = donroot.getElementsByTagName(hwver.DON_SERIALNO)
        for don in dons:
            if don.firstChild.nodeValue == serialno:
                ignored = True
                break

    if not ignored:
        if not donroot:
            donroot = doc.createElement(hwver.DON_IGNORE)
            root.appendChild(donroot)

        don = doc.createElement(hwver.DON_SERIALNO)
        txt = doc.createTextNode(serialno)
        don.appendChild(txt)
        donroot.appendChild(don)

    doc.writexml(open(hwver.HARDWARE_CONFIG_FILE+'~', 'w'), '', ' ', '\n')
    os.rename(hwver.HARDWARE_CONFIG_FILE+'~', hwver.HARDWARE_CONFIG_FILE)

def clear_don_ignore():
    """
    Remove any ignored DONs from the hardware.xml list.
    """
    doc = parse(hwver.HARDWARE_CONFIG_FILE)
    root = doc.documentElement
    donroot = root.getElementsByTagName(hwver.DON_IGNORE)
    if donroot:
        donroot[0].childNodes = []

    doc.writexml(open(hwver.HARDWARE_CONFIG_FILE+'~', 'w'), '', ' ', '\n')
    os.rename(hwver.HARDWARE_CONFIG_FILE+'~', hwver.HARDWARE_CONFIG_FILE)

def don_format():
    """
    Format the disks in the DON.
    """
    # initialize drive bay cache
    bay.cacheinit()
    # preformat all DON drives 
    return bool(bay.chaspreformatdrives(1) == 0)

def don_detach():
    """
    The DON will not remember the HEAD node and will re-format its drives.
    """
    hwver.chasnWriteOwnerSerialno(1, '')
    return True

def don_attach(head):
    hwver.chasnWriteOwnerSerialno(1, head)

def don_reset():
    """
    Forget about the DON, format the DON, and make the DON forget
    about the head node.
    """
    # don_format() requires ifs unmounted
    if isi.misc.ifs_mounted() == 1:
        print "Error:  ifs is mounted"
        return

    if don_detach():
        print "Detached"
    else:
        print "Unable to detach"
        return

    if don_format():
        print "Formatting complete"
    else:
        print "Unable to format"
        return

    if don_forget():
        print "Forget complete"
    else:
        print "Unable to forget"
        return

    # Restart isi_drive_d to update its bay count
    os.system("/usr/bin/isi_drive_d restart")

def don_forget():
    """
    The head node forgets about the DON.
    """
    hal.setChassisCount(1)
    clear_don_ignore()
    return True

def don_serial():
    return hal.getChassisSerialNumber(1)

def don_up():
    return hal.chassisIsActive(1)

def don_configure():
    '''
    Try to add expansion node to HEAD node: Expansion nodes have their owner 
    serial no written on them. If an expansion node owner serial no is different
    then head node serial no this expansion node used to belong to another head
    node, in which case the user  must decide whether to attach expansion node to
    new head, or ignore expansion node. If ignored the expansion node is recorded
    as an <expansion-ignore> elements in hardware.xml so that subsequent boots
    will also ignore this expansion node
    '''
    if hal.isAcceleratorClass():
        print "Skipping storage expansion node configuration on accelerator node."
        return

    head_serialno = hal.getChassisSerialNumber(0)
    if not head_serialno:
        print """
No serial number was found for storage node. This indicates a dongle fault.
        """.strip()
         
        print """
Storage node should continue to function normally, but you are advised
to contact Isilon support to investigate further.
        """.strip()

        print "Note! Skipping storage expansion node configuration."
        return
    
    print "Configuring storage expansion nodes..."

    # XXX
    # Currently we only handle a single DON
    # XXX
    #
    # Detect DONs, skip ignored ones.
    #
    if don_up():
        don_serialno = don_serial()
        don_owner_serialno = None
        chas_last_cnt = hal.getChassisCount()
        chas_new_cnt = 1
        detached = False
        attach = False
        reset = False
        if not don_serialno:
            print """
No serial number was found for storage expansion node. This indicates
a eeprom fault.
            """.strip()
            
            print """
Storage expansion node should continue to function normally, but you are advised
to contact Isilon support to investigate further.
            """.strip()
            
            don_serialno = 'with no known serial number'
            
        ignore = is_don_ignore(don_serialno)
        if not ignore:
            don_owner_serialno = hwver.chasnOwnerSerialno(1)
            # this DON does no have an owner
            if not don_owner_serialno:
                detached = True
            else:
                # this head owns the don
                if don_owner_serialno == head_serialno:
                    attach = True
                else:
                    # ask user to detach don from previous owner
                    if prompt_user_for_reset_don(don_owner_serialno,
                                                 head_serialno, don_serialno) == 1:
                        detached = True
                        reset = True
        if detached:
            print "Attaching storage expansion node %s." % don_serialno
            hwver.chasnWriteOwnerSerialno(1, head_serialno)
            attach = True
            
        if attach:
            chas_new_cnt = chas_new_cnt+1
        else:
            # user selected to ignore this don
            if not ignore:
                set_don_ignore(don_serialno)
            print "Ignoring storage expansion node %s." % don_serialno

        #update chassis count and synchronize with dongle
        if chas_new_cnt != chas_last_cnt:
            hal.setChassisCount(chas_new_cnt)

        # reset drives in DON if needed
        if reset and not don_format():
            print """
Error preformatting drives for storage expansion node %s.
                """.strip() % don_serialno
    else:
        print "No storage expansion node detected."

def usage():
    print """\
usage: isi_don_tool [-crdfh] [--configure] [--reset] [--detach]
                    [--forget] [--help]
       -c, --configure  Associate DON with head and head with DON.
                        May be invoked blindly.
       -r, --reset      Undo -c.  Detach, reformat drives, forget
       -d, --detach     The DON no longer has an owner
       -f, --forget     The head no longer has an EX

"""

def don_not_supported():
    print  "Storage expansion node not supported on this chassis type."

def handle_args(argv):
    import getopt

    try:
        opts, args = getopt.getopt(argv, "crdfh", ["configure", "reset",
                                                  "detach", "forget",
                                                  "help"])
    except getopt.GetoptError:
        return usage()

    if not hal.supportsDiskOnlyNode():
        return don_not_supported()

    action = usage
    for o, a in opts:
        if o == '-c' or o == '--configure':
            action = don_configure
        if o == '-r' or o == '--reset':
            action = don_reset
        if o == '-d' or o == '--detach':
            action = don_detach
        if o == '-f' or o == '--forget':
            action = don_forget
        if o == '-h' or o == '--help':
            action = usage
            break

    return action()

def main(argv):
    try:
        return handle_args(argv)
    except IOError, e:
        print"Error configuring storage expansion node:", \
                    os.strerror(e.errno) + ' (' + e.filename + ')'
        return 1
    except:
        raise
    return 0

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