#!/usr/bin/env python

# Copyright (C) 2011 Red Hat, Inc.
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
#
# Author: tasleson

# Note:
# Daemon for the libStorageMgmt library.

import os
import socket
import stat
import sys
import traceback
import signal
from lsm.external.daemon import Daemon
from lsm.common import Error, Info
import getopt
import select
import lsm.common
import pwd

BASE_DIR = '/var/run/lsm'
PID_FILE = BASE_DIR + '/lsmd.pid'
SOCKET_DIR = lsm.common.UDS_PATH
PLUGIN_DIR = '/usr/bin'
LSM_USER = 'libstoragemgmt'
daemon = None

(RUNNING, RESTART, EXIT) = (1,2,3)
serve_state = RUNNING

#If we are running as a new style system daemon
systemd = False

def Loud(msg):
    if not systemd:
        Error(msg)
    sys.stderr.write(msg + "\n")
    sys.exit(1)

class LsmDaemon(Daemon):

    def __init__(self, pid_file, no_fork = False):
        Daemon.__init__(self, pid_file, no_fork)
        self.no_fork = no_fork

    def setup_socket(self, file):
        """
        Setup a Unix domain socket for this file
        """
        try:
            os.unlink(file)
        except OSError:
            pass

        #TODO Look into changing the SELinux permissions.
        server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        server.bind(file)
        #Depending on umask we could get messed up here, so make sure
        #socket files have correct permissions so we can access them.
        os.chmod(file,(stat.S_IREAD|stat.S_IWRITE|stat.S_IRGRP|stat.S_IWGRP|stat.S_IROTH|stat.S_IWOTH))
        server.listen(1)
        return server

    def process_plugins(self, plugin_dir, socket_dir):
        """
        process the plug-in directory and return a list of entries ready to be used

        """
        server_sockets = {}

        Info("Scanning plug-in directory " + plugin_dir)

        try:
            files = os.listdir(plugin_dir)

            for e in files:
                if e[-10:] == '_lsmplugin':
                    name = e[:-10]
                    fqn = plugin_dir + os.sep + e

                    server_sockets[fqn] = self.setup_socket(
                        socket_dir + os.sep + name)
                    Info("Plugin " + fqn + " added.")

        except Exception:
            Error(str(traceback.format_exc()))

        return server_sockets

    @staticmethod
    def clean_sockets():
        """
        Remove all the socket files in directory
        """
        dir = SOCKET_DIR
        if len(dir):
            for f in os.listdir(dir):
                p = os.path.join(dir, f)
                mode = os.stat(p).st_mode

                if stat.S_ISSOCK(mode):
                    try:
                        os.unlink(p)
                    except OSError:
                        pass

    def exec_plugin(self, client, executable):
        """
        Executes the desired plug-in
        """
        pid = os.fork()

        if pid:
            #Parent
            client.close()
        else:
            #Client
            #Close the server sockets
            for e in self.servers:
                e.close()

            socket_fd = client.fileno()
            os.execl(executable, os.path.basename(executable), str(socket_fd))
            Error("Unable to execute plug-in = " + executable)

    def child_cleanup(self):
        """
        Check to see if any children have exited and get their exit value.
        """

        try:
            (d_pid, d_exit) = os.waitpid(-1, os.WNOHANG)
            while d_pid > 0:
                if d_exit:
                    Error("Plug-in process " + str(d_pid) + " exited with " +
                        str(os.WEXITSTATUS(d_exit)) + " waitpid value= " + str(d_exit))
                (d_pid, d_exit) = os.waitpid(-1, os.WNOHANG)
        except OSError:
            pass


    def _serving(self):
        """
        Wait for clients to connect on the unix domain socket and handle them.
        """
        global serve_state

        try:
            LsmDaemon.clean_sockets()

            s = self.process_plugins(PLUGIN_DIR, SOCKET_DIR)
            self.servers = s.values()

            if len(self.servers):
                while serve_state == RUNNING:
                    input_rdy = select.select(self.servers, [], [], 15)[0]

                    if len(input_rdy):
                        for c in input_rdy:
                            for k, v in s.iteritems():
                                if c == v:
                                    client = v.accept()[0]
                                    self.exec_plugin(client, k)

                    self.child_cleanup()
            else:
                Error("No plug-ins found in directory: " + PLUGIN_DIR)
                serve_state = EXIT
        except select.error:
            #Select got interrupted by a signal
            pass
        except Exception:
            Error(str(traceback.format_exc()))
        finally:
            self.cleanup()

    def serve(self):
        """
        Main daemon worker part.
        """
        global serve_state
        while serve_state != EXIT:
            if serve_state == RESTART:
                Info("Reloading plug-ins")
                serve_state = RUNNING

            self._serving()

        Info("Service exiting")

    def cleanup(self):
        Info("Closing and removing sockets")
        for e in self.servers:
            e.close()
        LsmDaemon.clean_sockets()

    def run(self):
        self.serve()

def drop_privileges():
    """
    Give up root privs as we don't need them at this time
    """
    if not systemd:
        try:
            pw = pwd.getpwnam(LSM_USER)
            if not os.geteuid():
                os.setgid(pw.pw_gid)
                os.setuid(pw.pw_uid)
            elif pw.pw_uid != os.getuid():
                print("Warn: Daemon not running as correct user")
        except KeyError:
            print("Warn: Missing %s user, running as existing user!" % LSM_USER)

def flight_check():
    """
    Make sure we can read and write to the areas of interest.
    """
    if os.path.exists(PID_FILE) and not os.path.isfile(PID_FILE):
        Loud("--pidfile %s exits but is not a file" % PID_FILE)

    pid_dir = os.path.dirname(PID_FILE)
    if not os.path.exists(pid_dir):
        Loud("Directory %s does not exist for pid file, exiting" % pid_dir)

    if not os.access(pid_dir, os.W_OK):
        Loud("No write permission for pid file dir %s exiting!" % pid_dir)

    if not os.path.exists(PLUGIN_DIR):
        Loud("Plug-in directory does not exist %s exiting" % PLUGIN_DIR)

    if not os.path.exists(SOCKET_DIR):
        Loud("Socket directory does not exist %s exiting" % PLUGIN_DIR)

    if  not os.access(PLUGIN_DIR, os.R_OK):
        Loud("No permission to read dir %s exiting" % PLUGIN_DIR)

    if not os.access(SOCKET_DIR, os.W_OK):
        Loud("No write permission to directory %s exiting!" % SOCKET_DIR)

def usage():
    print 'libStorageMgmt plug-in daemon.'
    print "lsmd --pidfile <file> --plugindir <directory> --socketdir <dir>"
    print "    --pidfile   = The process id file for the daemon"
    print "    --plugindir = The directory where the plugins are located"
    print "    --socketdir = The directory where the Unix domain sockets will be created"
    print "    --operation = [start|stop|restart|force-reload]"
    print "    -v          = Verbose logging"
    print "    -d          = new style daemon (systemd)"
    sys.exit(2)

def sig_handler(number, frame):
    global serve_state
    if number == signal.SIGTERM:
        serve_state = EXIT
    elif number == signal.SIGHUP:
        serve_state = RESTART

def install_sh():
    signal.signal(signal.SIGTERM, sig_handler)
    signal.signal(signal.SIGHUP, sig_handler)

if __name__ == "__main__":
    op = "start"

    try:
        opts, args = getopt.getopt(sys.argv[1:], "hvd", ["help", "pidfile=",
                                                        "plugindir=",
                                                        "socketdir=",
                                                        "operation="])
    except getopt.GetoptError, err:
        # print help information and exit:
        print str(err)
        usage()
        sys.exit(2)

    for o, a in opts:
        if o in ("-h", "--help"):
            usage()
        elif o in ("--pidfile"):
            PID_FILE = a
        elif o in ("--plugindir"):
            PLUGIN_DIR = a
        elif o in ("--socketdir"):
            SOCKET_DIR = a
        elif o in "-v":
            lsm.common.LOG_VERBOSE = True
        elif o in "-d":
            systemd = True
        elif o in ("--operation"):
            op = a

    if PLUGIN_DIR is None:
        Loud("--plugindir is a required argument")
        sys.exit(2)

    #Install signal handlers
    install_sh()

    #Run as non-root
    drop_privileges()

    #Check to make sure we can create socket files etc as needed before
    #we try to become a daemon
    flight_check()

    daemon = LsmDaemon(PID_FILE, systemd)

    if 'start' == op:
        daemon.start()
    elif 'stop' == op:
        daemon.stop()
        LsmDaemon.clean_sockets()
    elif 'restart' == op:
        daemon.restart()
    elif 'force-reload' == op:
        daemon.restart()
    else:
        print "Invalid operation", op
        sys.exit(2)

    sys.exit(0)
