# vim: tabstop=4 shiftwidth=4 softtabstop=4

# Copyright (c) 2011 OpenStack, LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Helper script for starting/stopping/reloading Glance server programs.
Thanks for some of the code, Swifties ;)
"""

from __future__ import with_statement

import argparse
import errno
import fcntl
import os
import resource
import signal
import subprocess
import sys
import tempfile
import time

# If ../glance/__init__.py exists, add ../ to Python search path, so that
# it will override what happens to be installed in /usr/(local/)lib/python...
possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
                                   os.pardir,
                                   os.pardir))
if os.path.exists(os.path.join(possible_topdir, 'glance', '__init__.py')):
    sys.path.insert(0, possible_topdir)

from oslo.config import cfg

from glance.common import config

CONF = cfg.CONF

ALL_COMMANDS = ['start', 'status', 'stop', 'shutdown', 'restart',
                'reload', 'force-reload']
ALL_SERVERS = ['api', 'registry', 'scrubber']
GRACEFUL_SHUTDOWN_SERVERS = ['glance-api', 'glance-registry',
                             'glance-scrubber']
MAX_DESCRIPTORS = 32768
MAX_MEMORY = (1024 * 1024 * 1024) * 2  # 2 GB
USAGE = """%(prog)s [options] <SERVER> <COMMAND> [CONFPATH]

Where <SERVER> is one of:

    all, api, registry, scrubber

And command is one of:

    start, stop, shutdown, restart, reload, force-reload

And CONFPATH is the optional configuration file to use."""

exitcode = 0


def gated_by(predicate):
    def wrap(f):
        def wrapped_f(*args):
            if predicate:
                return f(*args)
            else:
                return None
        return wrapped_f
    return wrap


def pid_files(server, pid_file):
    pid_files = []
    if pid_file:
        if os.path.exists(os.path.abspath(pid_file)):
            pid_files = [os.path.abspath(pid_file)]
    else:
        if os.path.exists('/var/run/glance/%s.pid' % server):
            pid_files = ['/var/run/glance/%s.pid' % server]
    for pid_file in pid_files:
        pid = int(open(pid_file).read().strip())
        yield pid_file, pid


def do_start(verb, pid_file, server, args):
    if verb != 'Respawn' and pid_file == CONF.pid_file:
        for pid_file, pid in pid_files(server, pid_file):
            if os.path.exists('/proc/%s' % pid):
                print ("%s appears to already be running: %s" %
                       (server, pid_file))
                return
            else:
                print "Removing stale pid file %s" % pid_file
                os.unlink(pid_file)

        try:
            resource.setrlimit(resource.RLIMIT_NOFILE,
                               (MAX_DESCRIPTORS, MAX_DESCRIPTORS))
            resource.setrlimit(resource.RLIMIT_DATA,
                               (MAX_MEMORY, MAX_MEMORY))
        except ValueError:
            action = 'increase file descriptor limit'
            print 'Unable to %s.  Running as non-root?' % action
        os.environ['PYTHON_EGG_CACHE'] = '/tmp'

    def write_pid_file(pid_file, pid):
        fp = open(pid_file, 'w')
        fp.write('%d\n' % pid)
        fp.close()

    def redirect_to_null(fds):
        with open(os.devnull, 'r+b') as nullfile:
            for desc in fds:  # close fds
                try:
                    os.dup2(nullfile.fileno(), desc)
                except OSError:
                    pass

    def redirect_to_syslog(fds, server):
        log_cmd = 'logger -t "%s[%d]"' % (server, os.getpid())
        process = subprocess.Popen(log_cmd,
                                   shell=True,
                                   stdin=subprocess.PIPE)
        for desc in fds:  # pipe to logger command
            try:
                os.dup2(process.stdin.fileno(), desc)
            except OSError:
                pass

    def redirect_stdio(server, capture_output):
        input = [sys.stdin.fileno()]
        output = [sys.stdout.fileno(), sys.stderr.fileno()]

        redirect_to_null(input)
        if capture_output:
            redirect_to_syslog(output, server)
        else:
            redirect_to_null(output)

    @gated_by(CONF.capture_output)
    def close_stdio_on_exec():
        fds = [sys.stdin.fileno(), sys.stdout.fileno(), sys.stderr.fileno()]
        for desc in fds:  # set close on exec flag
            fcntl.fcntl(desc, fcntl.F_SETFD, fcntl.FD_CLOEXEC)

    def launch(pid_file, conf_file=None, capture_output=False, await_time=0):
        args = [server]
        print '%sing %s' % (verb, server),
        if conf_file:
            args += ['--config-file', conf_file]
            print 'with %s' % conf_file,
        print

        close_stdio_on_exec()

        pid = os.fork()
        if pid == 0:
            os.setsid()
            redirect_stdio(server, capture_output)
            try:
                os.execlp('%s' % server, *args)
            except OSError as e:
                msg = 'unable to launch %s. Got error: %s' % (server, e)
                sys.exit(msg)
            sys.exit(0)
        else:
            write_pid_file(pid_file, pid)
            await_child(pid, await_time)
            return pid

    @gated_by(CONF.await_child)
    def await_child(pid, await_time):
        bail_time = time.time() + await_time
        while time.time() < bail_time:
            reported_pid, status = os.waitpid(pid, os.WNOHANG)
            if reported_pid == pid:
                global exitcode
                exitcode = os.WEXITSTATUS(status)
                break
            time.sleep(0.05)

    conf_file = None
    if args and os.path.exists(args[0]):
        conf_file = os.path.abspath(os.path.expanduser(args[0]))

    return launch(pid_file, conf_file, CONF.capture_output, CONF.await_child)


def do_check_status(pid_file, server):
    if os.path.exists(pid_file):
        pid = open(pid_file).read().strip()
        print "%s running: %s" % (server, pid)
    else:
        print "No %s running" % server


def get_pid_file(server, pid_file):
    pid_file = (os.path.abspath(pid_file) if pid_file else
                '/var/run/glance/%s.pid' % server)
    dir, file = os.path.split(pid_file)

    if not os.path.exists(dir):
        try:
            os.makedirs(dir)
        except OSError:
            pass

    if not os.access(dir, os.W_OK):
        fallback = os.path.join(tempfile.mkdtemp(), '%s.pid' % server)
        msg = ('Unable to create pid file %s.  Running as non-root?\n'
               'Falling back to a temp file, you can stop %s service using:\n'
               '  %s %s stop --pid-file %s'
               % (pid_file, server, __file__, server, fallback))
        print msg
        pid_file = fallback

    return pid_file


def do_stop(server, args, graceful=False):
    if graceful and server in GRACEFUL_SHUTDOWN_SERVERS:
        sig = signal.SIGHUP
    else:
        sig = signal.SIGTERM

    did_anything = False
    pfiles = pid_files(server, CONF.pid_file)
    for pid_file, pid in pfiles:
        did_anything = True
        try:
            os.unlink(pid_file)
        except OSError:
            pass
        try:
            print 'Stopping %s  pid: %s  signal: %s' % (server, pid, sig)
            os.kill(pid, sig)
        except OSError:
            print "Process %d not running" % pid
    for pid_file, pid in pfiles:
        for _junk in xrange(150):  # 15 seconds
            if not os.path.exists('/proc/%s' % pid):
                break
            time.sleep(0.1)
        else:
            print ('Waited 15 seconds for pid %s (%s) to die; giving up' %
                   (pid, pid_file))
    if not did_anything:
        print 'No %s running' % server


def add_command_parsers(subparsers):
    cmd_parser = argparse.ArgumentParser(add_help=False)
    cmd_subparsers = cmd_parser.add_subparsers(dest='command')
    for cmd in ALL_COMMANDS:
        parser = cmd_subparsers.add_parser(cmd)
        parser.add_argument('args', nargs=argparse.REMAINDER)

    for server in ALL_SERVERS:
        full_name = 'glance-' + server

        parser = subparsers.add_parser(server, parents=[cmd_parser])
        parser.set_defaults(servers=[full_name])

        parser = subparsers.add_parser(full_name, parents=[cmd_parser])
        parser.set_defaults(servers=[full_name])

    parser = subparsers.add_parser('all', parents=[cmd_parser])
    parser.set_defaults(servers=['glance-' + s for s in ALL_SERVERS])


def main():
    global exitcode

    opts = [
            cfg.SubCommandOpt('server',
                              title='Server types',
                              help='Available server types',
                              handler=add_command_parsers),
            cfg.StrOpt('pid-file',
                       metavar='PATH',
                       help='File to use as pid file. Default: '
                       '/var/run/glance/$server.pid'),
            cfg.IntOpt('await-child',
                       metavar='DELAY',
                       default=0,
                       help='Period to wait for service death '
                            'in order to report exit code '
                            '(default is to not wait at all)'),
            cfg.BoolOpt('capture-output',
                        default=False,
                        help='Capture stdout/err in syslog '
                        'instead of discarding'),
            cfg.BoolOpt('respawn',
                        default=False,
                        help='Restart service on unexpected death'),
    ]
    CONF.register_cli_opts(opts)

    config.parse_args(usage=USAGE)

    @gated_by(CONF.await_child)
    @gated_by(CONF.respawn)
    def mutually_exclusive():
        sys.stderr.write('--await-child and --respawn are mutually exclusive')
        sys.exit(1)

    mutually_exclusive()

    @gated_by(CONF.respawn)
    def anticipate_respawn(children):
        while children:
            pid, status = os.wait()
            if pid in children:
                (pid_file, server, args) = children.pop(pid)
                running = os.path.exists(pid_file)
                one_second_ago = time.time() - 1
                bouncing = (running and
                            os.path.getmtime(pid_file) >= one_second_ago)
                if running and not bouncing:
                    args = (pid_file, server, args)
                    new_pid = do_start('Respawn', *args)
                    children[new_pid] = args
                else:
                    rsn = 'bouncing' if bouncing else 'deliberately stopped'
                    print 'Supressed respawn as %s was %s.' % (server, rsn)

    if CONF.server.command == 'start':
        children = {}
        for server in CONF.server.servers:
            pid_file = get_pid_file(server, CONF.pid_file)
            args = (pid_file, server, CONF.server.args)
            pid = do_start('Start', *args)
            children[pid] = args

        anticipate_respawn(children)

    if CONF.server.command == 'status':
        for server in CONF.server.servers:
            pid_file = get_pid_file(server, CONF.pid_file)
            do_check_status(pid_file, server)

    if CONF.server.command == 'stop':
        for server in CONF.server.servers:
            do_stop(server, CONF.server.args)

    if CONF.server.command == 'shutdown':
        for server in CONF.server.servers:
            do_stop(server, CONF.server.args, graceful=True)

    if CONF.server.command == 'restart':
        for server in CONF.server.servers:
            do_stop(server, CONF.server.args)
        for server in CONF.server.servers:
            pid_file = get_pid_file(server, CONF.pid_file)
            do_start('Restart', pid_file, server, CONF.server.args)

    if (CONF.server.command == 'reload' or
        CONF.server.command == 'force-reload'):
        for server in CONF.server.servers:
            do_stop(server, CONF.server.args, graceful=True)
            pid_file = get_pid_file(server, CONF.pid_file)
            do_start('Restart', pid_file, server, CONF.server.args)

    sys.exit(exitcode)
