#! /usr/libexec/isilon/isi_group_change_d

# Copyright C 2002-2003 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.

'''
 $Id: isi_group_change_d 245029 2012-05-20 23:23:09Z rlibby $
'''


import errno
import os
import select
import signal
import socket
import sys
import time
from syslog import openlog, syslog, LOG_INFO, LOG_NOTICE, LOG_ERR, LOG_DAEMON
from stat import ST_DEV

import isi.fs.event
import isi.app.daemon
import isi.misc
import isi.sys.version as sysver

from isi.app.lib.process import madvise, MADV_PROTECT
from isi.fs.restripe.group import GroupInfo
from isi.fs.restripe.util import setSigHandlers, setSigHandlersDefault

HAS_SBQ = "efs.gmp.has_super_block_quorum"

################ group change routines for modification ################

def AddedNodes( added, log=True):
    '''Add commands here that need to run when nodes are added.
    
       Returns:
         True if we need to retry
         False if everything is OK

    '''
    if log:
        syslog(LOG_NOTICE, "checking OneFS")

    if (isi.misc.ifs_ready()):
        syslog(LOG_NOTICE, "executing group add actions.")
        ret = os.system("/bin/sh /etc/rc.d/isi_post_ifs")
        if ((not os.WIFEXITED(ret)) or (os.WEXITSTATUS(ret) != 0)) and log:
            syslog(LOG_NOTICE, "group add action -- " \
                    "Error running isi_post_ifs. [%d]" % ret)
        return False
    else:
        if log:
            syslog(LOG_NOTICE, "OneFS not ready -- " \
                    "group add actions will be retried.")

    return True

def RemovedNodes( removed, log=True ):
    '''Add commands here that need to run when nodes are removed.'''

    ret = os.system("/bin/sh /usr/libexec/isilon/on_group_change")
    if os.WEXITSTATUS(ret) != 0 and log:
        syslog(LOG_NOTICE, "group remove action -- non-zero exit-code.")

    return False


################ Group Change Daemon ################

class GroupchangeDaemon:
    '''Daemon to execute commands on group change.'''

    def __init__(self):
        if os.path.basename(sys.argv[0]) == 'python':
            name = 'isi_group_change_d'
        else:
            name = os.path.basename(sys.argv[0])
        openlog( name, 0, LOG_DAEMON )
        self.event_listener = None
        self.upnodes = []
        self.up_pending = False
        self.down_pending = False

        # Start with no nodes.  This is to pick up any nodes that may have
        # been added since boot.  It also gives us a chance to re-run node
        # add actions after quorum has been achieved.

        self.lastup = []

        # Set signal handlers.
        setSigHandlers({
            signal.SIGHUP  : self.handleSigHup,
            signal.SIGTERM : self.handleSigTerm,
            signal.SIGINT  : self.handleSigInt,
            signal.SIGUSR1 : self.handleSigUsr1,
            signal.SIGUSR2 : self.handleSigUsr2
        })

        # Daemonize.
        if not debug:
            isi.app.daemon.startstop()

        # Protect from OOM killer
        madvise(MADV_PROTECT)

    def main(self):
        if os.stat("/")[ST_DEV] == os.stat("/ifs")[ST_DEV]:
            # /ifs is not mounted, so print an error and exit
            syslog(LOG_NOTICE, "A mounted /ifs is required.")

            self.stop()
            self.exit(1)

        syslog(LOG_NOTICE, "starting up.")
        self.has_sbq = isi.misc.sysctlbyname(HAS_SBQ)
        while 1:
            # If we fail to start/connect, we need to retry later.
            try:
                # Connect to the event system.
                self.event_listener = isi.fs.event.listener()
                self.event_listener.subscribe("gmp")
            except SystemExit:
                break
            except Exception, e:
                syslog(LOG_ERR, "Error starting: %s" % str(e))
                raise

            # Handle change to initial mounted state.
            self.handleGroupChange()
                    
            try:
                while 1:
                    self.service()
            except SystemExit:
                break
            except Exception, e:
                syslog(LOG_ERR, "Encountered error: %s" % str(e))
                self.stop()
                raise

    def service(self):
        try:
            select.select([self.event_listener.fileno()], [], [], 1)
        except select.error, (error, strerror):
            if error == errno.ENODEV:
                print("select raised ENODEV")
            elif error == errno.EINTR:
                pass
            else:
                raise

        event = self.event_listener.read()
        if event: # Either a group change, or we've lost events
            self.handleGroupChange()
            GroupInfo().handleGroupChange()
        self.handlePeriodic()

    ################ Group change handling routines ################

    def update(self):
        self.ctime = time.time()
        self.lastup = self.upnodes
        try:
            GroupInfo().update()
            self.upnodes = GroupInfo().getNodeList()
        except:
            self.upnodes = []

    def handlePeriodic(self):
        if self.up_pending:
            self.up_pending = AddedNodes( None, False )
        if self.down_pending:
            self.down_pending = RemovedNodes( None, False )

    def handleGroupChange(self):
        self.update()

        # figure out what the changes are.
        up = []
        down = []
        new = self.upnodes[:]
        old = self.lastup[:]

        # while either new or old list are new loop
        while len(new) + len(old) > 0:
            # no infinite looping.  all branches delete or break
            # if new is empty, the rest of old is down
            if len(new) == 0:
                down += old
                break;
            # if old is empty, the rest of new is up
            elif len(old) == 0:
                up += new
                break;
            # if the two nodes are equal, delete both
            elif new[0] == old[0]:
                del new[0]
                del old[0]
            # if old is greater, new is up
            elif new[0] < old[0]:
                up += [ new[0] ]
                del new[0]
            # else new is greater, old is down
            else:
                down += [ old[0] ]
                del old[0]

        if len(down):
            print 'Removed nodes: ', down, ' old(', self.lastup, ') new(', self.upnodes, ')'
            self.down_pending = RemovedNodes( down )

        if len(up):
            print 'Added nodes', up, ' old(', self.lastup, ') new(', self.upnodes, ')'
            self.up_pending = AddedNodes( up )


        # If we just got superblock quorum, run isi_post_ifs if we haven't done so already
        has_sbq = isi.misc.sysctlbyname(HAS_SBQ)
        if self.has_sbq == 0L and has_sbq == 1L \
                and len(up) == 0:
            ret = os.system("/bin/sh /etc/rc.d/isi_post_ifs")
            if ((not os.WIFEXITED(ret)) or (os.WEXITSTATUS(ret) != 0)) and log:
                syslog(LOG_ERR, "Error running isi_post_ifs. [%d]" % ret)
        elif has_sbq == 1L:
            # still may need to update node info cache
            syslog(LOG_NOTICE, "Updating node info cache")
            sysver.update_cache(skip_if_current=True)

        self.has_sbq = has_sbq

    ################ shutdown routines ################

    def stop(self):
        # Nothing to do here
        syslog(LOG_INFO, "Stopping daemon")

    def shutdown(self, reason=None):
        if reason:
            syslog(LOG_NOTICE, "Shutting down due to \"%s\"" % reason)

        # Set handlers back to default
        setSigHandlersDefault([signal.SIGHUP, signal.SIGTERM,
                              signal.SIGINT, signal.SIGUSR1])
        self.stop()
        sys.exit(0)


    ################ Signal stuff ################

    def handleSigHup(self, signum, frame):
        # SIGHUP ignored for now.
        pass
        
    def handleSigTerm(self, signum, frame):
        self.shutdown("SIGTERM")

    def handleSigInt(self, signum, frame):
        self.shutdown("SIGINT")

    def handleSigUsr1(self, signum, frame):
        '''USR1 makes daemon think all nodes where down and thus up nodes
        transitioning to up.'''
        syslog(LOG_NOTICE,
                "Received SIGUSR1, restarting as if all nodes were down.")
        print("Received SIGUSR1, restarting as if all nodes were down.")
        self.upnodes = []
        self.handleGroupChange()

    def handleSigUsr2(self, signum, frame):
        '''USR2 makes daemon think all nodes where up and thus down nodes
        are transitioning down.'''
        syslog(LOG_NOTICE,
                "Received SIGUSR2, restarting as if all nodes were up.")
        print("Received SIGUSR2, restarting as if all nodes were up.")

        try:
            GroupInfo().update()
            self.upnodes = GroupInfo().getNodeList() + \
                    GroupInfo().getDownNodeList()
            self.handleGroupChange()
        except:
            print('Error: could not get group information '
                    'to determine up nodes')


    ################ Node parsing stuff ################

if __name__ == '__main__':
    debug = 0
    if len(sys.argv) == 2 and sys.argv[1] == 'debug':
        debug = 1

    if isi.app.daemon.validAction() or debug == 1:
        gcd = GroupchangeDaemon()
        gcd.main()
    else:
        print("Usage: %s start|stop|restart|debug  (debug doesn't fork)"
                % sys.argv[0])
        sys.exit(0)

# vim: set filetype=python:
