#!/usr/bin/env python

#--
# Copyright 2010 Red Hat, Inc.
#
# 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.
#++


from datetime import datetime
from subprocess import Popen, PIPE
from collections import defaultdict

import optparse
import os
import sys
import re

STATE_OK = 0
STATE_WARNING = 1
STATE_CRITICAL = 2
STATE_UNKNOWN = 3
STATE_DEPENDENT = 4

def get_options():
    """ command-line options """

    usage = "usage: %prog [options]"
    OptionParser = optparse.OptionParser
    parser = OptionParser(usage)

    parser.add_option("-v", "--verbose", action="store_true", default=False, \
            dest="verbose", help="Print additional details.")
    parser.add_option("--validate", action="store_true", default=False, \
            dest="validate", help="Perform additional sanity checks.")

    opts, args = parser.parse_args()
    return opts, args

def proclist():
    ''' fetches a process list and their uids '''
    # returns lines of "uid cmd"
    ret, out = run('/bin/ps axwo uid=,args=')
    arr = []
    for line in out:
        if line.strip() == '':
            continue
        l = line.lstrip(" ").split(" ")
        uid = l.pop(0)
        cmd = " ".join(l)
        if uid.strip() == '':
            print line
        d = { 'uid' : uid, 'cmd' : cmd }
        arr.append(d)
    return arr

all_idled_apps=None
def is_idled(appid):
    ''' returns a boolean saying if the app is currently idled '''
    global all_idled_apps
    if all_idled_apps == None:
        ret, all_idled_apps = run("/usr/sbin/oo-admin-ctl-gears listidle | /bin/awk '{print $1}'")
        if ret != 0:
            return False # an error occurred, bail
        all_idled_apps.remove('')

    return (appid in all_idled_apps)

dict_uid_count=None
def num_procs_with_uid(uid, proclist, opts):
    ''' return count of the number of apache or java processes associated with a given uid. '''
    global dict_uid_count
    if dict_uid_count == None:
        dict_uid_count = defaultdict(int)
        for d in proclist:
          dict_uid_count[d['uid']] += 1
    return dict_uid_count[uid]

def user_list():
    ''' fetches a list of usernames and their uids '''
    # returns lines of "username:uid"
    ret, out = run('grep ":OpenShift guest:" /etc/passwd | cut -d: -f"1 3"')
    dct = {}
    for line in out:
        if line:
            l = line.split(":")
            dct[l[1]] = l[0]
    return dct

def run(cmd, workdir=None):
    ''' run commands in a subprocess and wait for the return code.
        optional: provide a working dir for the command.
    '''
    cwd = None
    if workdir:
        cwd = os.getcwd()
        os.chdir(workdir)

    proc = Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE)
    output = proc.communicate()
    output = output[0].split('\n')

    if cwd:
        os.chdir(cwd)

    return proc.returncode, output

def percent(a, b):
    return float(a)/float(b)*100.0

def cartridge_types(homedir):
    return_code, types = run("ls |egrep -v 'app-root|git'", homedir)
    return types

def get_app_type(app_path, app_name="unknown"):
    ''' try to figure out what sort of openshift app is inside a given path.
    Warning: this is a bit naive.
    '''

    installed = cartridge_types(app_path)
    if 0 == len(installed):
        return "unknown"

    candidates = set(["diy", "jbossas", "jbosseap", "jbossews", "nodejs", "perl", \
        "php", "python", "ruby", "zend"]).intersection(installed)
    if 0 < len(candidates):
        return candidates.pop()

    candidates = set(["mongodb", "mysql", "postgresql", "switchyard", \
        "jenkins"]).intersection(installed)
    if 0 < len(candidates):
        return candidates.pop()

    candidates = set(["metrics", "rockmongo", "10gen-mms-agent", \
        "cron", "haproxy", "jenkins-client", "phpmyadmin"]).intersection(installed)
    if 0 < len(candidates):
        return candidates.pop()

    return "unknown"

def validate(uuid):
    ''' given a user's uuid, verify an app exists. '''
    app_path = "/var/lib/openshift/%s" % uuid

    # Is there a /git directory?
    if not os.path.isdir(app_path+"/git"):
        return "Path %s/git not found." % app_path

    # Is there something inside the /git directory?
    dirs = os.listdir(app_path+"/git")
    if len(dirs) == 0:
        return "Path %s/git is empty." % app_path

    # Under /git should be appname.git
    app_name = None
    for d in dirs:
        if re.search('\.git$', d):
            app_name = d.replace(".git", "")

    # Is there stuff inside git/appname.git?
    gitfiles = os.listdir(app_path+"/git/"+app_name+".git")
    if len(gitfiles) == 0:
        return "Path %s/git/%s.git is empty." % (app_path, app_name)

    # Chances are good that this is a git repo. What's the last commit?
    last_commit = run('git show HEAD | grep Date | sed "s/Date:   //"',
            app_path+"/git/"+app_name+".git")[1][0]
    # %z is broken in python-2.6.6-20.el6
    # see also: http://bugs.python.org/issue6641
    last_commit = datetime.strptime(last_commit[:-6], "%a %b %d %H:%M:%S %Y")
    time_diff = datetime.now() - last_commit

    # do any cartridges exists?
    types = cartridge_types(app_path)
    if 0 >= len(types):
        return "Path %s does not contain any cartridges." % (app_path)

    # is there stuff in cartridges?
    for cartridge in types:
        cartridge_path = app_path + "/" + cartridge
        app_files = os.listdir(cartridge_path)
        if len(app_files) == 0:
            return "Path %s is empty." % (cartridge_path)

    # great! what is it?
    app_type = get_app_type(app_path, app_name)

    # tell the world what we've found!
    return """
    Last updated: %s (%s days ago)
    %s (%s) seems okay.
""" % ( last_commit.strftime("%b %d %Y"), \
        time_diff.days, \
        app_name, \
        app_type )

def status_output(up, idle, half_idled, tot, pct, state):
    st = "UNK"
    if state == STATE_OK:
        st = "OK"
    elif state == STATE_WARNING:
        st = "WARN"
    elif state == STATE_CRITICAL:
        st = "CRIT"
    else:
        pass # else is for completeness.

    print "%s running, %s idled, %s half-idled for a total %s of %s (%.2f %%)" % (up, idle, half_idled, (up + idle), tot, pct)
    sys.exit(0)

if "__main__" in __name__:
    opts, args = get_options()

    procs = proclist()
    users = user_list()
    num_apps = len(users)

    if num_apps  == 0:
        print "OK: No apps found. Nothing to monitor."
        sys.exit(STATE_OK)

    running_apps = 0
    idled_apps = 0
    half_idled_apps = 0
    for uid in users:
        num_procs = num_procs_with_uid(uid, procs, opts)
        if opts.verbose:
            print "Found %s processes for uid %s, username %s." % (num_procs, uid, users[uid])
        if opts.validate:
            print "Validation for uid %s (%s): %s" % (uid, users[uid],
                    str(validate(users[uid])))
        is_running = False
        if num_procs > 0:
            running_apps += 1
            is_running = True

        if is_idled(users[uid]):
            if is_running:
                half_idled_apps += 1
            else:
                idled_apps += 1

    pct = percent(running_apps + idled_apps, num_apps)

    #XXX: this could probably be made to be user-definable via cli options.
    if num_apps < 5:
        # never show problems if there's fewer than 5 apps found.
        status_output(running_apps, idled_apps, half_idled_apps, num_apps, pct, STATE_OK)
    else:
        if pct >= 80.0:
            status_output(running_apps, idled_apps, half_idled_apps, num_apps, pct, STATE_OK)
        elif pct < 80.0 and pct >= 50.0:
            status_output(running_apps, idled_apps, half_idled_apps, num_apps, pct, STATE_WARNING)
        else:
            status_output(running_apps, idled_apps, half_idled_apps, num_apps, pct, STATE_CRITICAL)
