#!/bin/sh
#
# Author: Mark Lamourine <markllama@gmail.com>
# Copyright 2012
#
# 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.

# This script checks the status of a host that is running an Openshift Origins
# Broker service.
#
# Check that the broker components are installed and communicating properly
#
OSO_CONF_DIR=${OSO_CONF_DIR:=/etc/openshift}

OSO_BROKER_ROOT=${OSO_BROKER_ROOT:=/var/www/openshift/broker}

OSO_CFG_DIR=${OSO_BROKER_ROOT}/config
OSO_HTTPD_DIR=${OSO_BROKER_ROOT}/httpd
OSO_HTTPD_CONF_DIR=${OSO_HTTPD_DIR}/conf.d
OSO_ENV_DIR=${OSO_CFG_DIR}/environments
OSO_INI_DIR=${OSO_CFG_DIR}/initializers

if [ -e '/etc/openshift/development' ] ; then
  OSO_ENVIRONMENT=${OSO_ENVIRONMENT:=development}
else
  OSO_ENVIRONMENT=${OSO_ENVIRONMENT:=production}
fi

AUTH_PLUGINS="mongo"
NAME_PLUGINS="bind"
MESG_PLUGINS="mcollective"

AUTH_MODULE_PATTERN="rubygem-openshift-origin-auth-%NAME%"
NAME_MODULE_PATTERN="rubygem-openshift-origin-dns-%NAME%"
MESG_MODULE_PATTERN="rubygem-openshift-origin-msg-broker-%NAME%"

DEFAULT_DATA_SERVICE="mongo"
DEFAULT_AUTH_SERVICE="mongo"
DEFAULT_NAME_SERVICE="bind"
DEFAULT_MESG_SERVICE="mcollective"

DEFAULT_PACKAGES="
  ruby
  rubygems
  rubygem-rails
  rubygem-passenger
  rubygem-openshift-origin-common
  rubygem-openshift-origin-controller
  openshift-origin-broker 
"

DEFAULT_SERVICES="openshift-broker"

# Depends on OSO_BROKER_ROOT in ruby lib search list
OSO_RUBY_LIBS="openshift-origin-controller config/application"

if [ -z "$PACKAGES" ]
then
    PACKAGES="$DEFAULT_PACKAGES"
else
    echo WARNING: ENV overrides PACKAGES >&2
fi

if [ -z "$SERVICES" ]
then
    SERVICES="$DEFAULT_SERVICES"
else
    echo WARNING: ENV overrides SERVICES >&2
fi

BROKER_REST_API_BASE="https://localhost/broker/rest/"

# ============================================================================
#
# Utility Functions
#
# ============================================================================

function verbose() {
    # MESSAGE=$*

    if [ -n "$VERBOSE" ]
    then
	echo "INFO: $*"
    fi
}

function notice() {
    # MESSAGE=$*
    echo "NOTICE: $*"
}

function fail() {
    # MESSAGE=$*
    echo "FAIL: $*" >&2
    STATUS=$(($STATUS + 1))
}


# ==========================================================================
#
# ==========================================================================
function probe_version() {
    # Find out who owns /var/www/openshift/broker

    BROKER_RPM=$(rpm -qf $OSO_BROKER_ROOT --qf '%{NAME}\n' 2>/dev/null)
    if [ -z "$BROKER_RPM" ]
    then
      fail "broker Rails app root ${OSO_BROKER_ROOT} does not exist: no broker package"
    else
      verbose "Broker package is: $BROKER_RPM"
    fi
}

function check_ruby_requirements() {
    verbose checking ruby requirements
    if [ ! -x /usr/bin/ruby ]
    then
	fail ruby is not installed or not executable
	return
    fi
    for OSO_RUBY_LIB in $*
    do
	verbose checking ruby requirements for $OSO_RUBY_LIB
        GEM_ERROR=`ruby -I ${OSO_BROKER_ROOT} -r rubygems -- <<EOF
            require 'bundler'
            begin
              require '$OSO_RUBY_LIB'
            rescue Bundler::GemNotFound => gem_error
              puts "Gem Not Found for $OSO_RUBY_LIB: #{gem_error}"
              exit 1
            end
EOF`
	if [ $? -ne 0 ]
        then
	    fail module $OSO_RUBY_LIB -- "$GEM_ERROR"
        fi 
    done
}

#
# Get a single value from the Rails application.
# Yes, this is inefficient, but the point is to not depend on the system
# you're examining (Ruby/Rails) to confirm itself.
#
function get_application_value() {
  # $1 = RUBY_VARNAME
  if [ -z "$OSO_BROKER_ROOT" ]
  then
      fail OSO_BROKER_ROOT is unset.  Cannot load app configuration
      exit $STATUS
  fi
  if [ -z "$OSO_ENVIRONMENT" ]
  then
      fail OSO_ENVIRONMENT is unset.  Cannot load app configuration
      exit $STATUS
  fi
  if which ruby 2>/dev/null >/dev/null
  then
      ruby -I ${OSO_BROKER_ROOT} -r rubygems -- <<EOF 2>&1
        require 'bundler'
        require 'rails'
        begin
          require 'openshift-origin-controller'
          require 'config/application'
          #require 'config/initializers/broker'
          require 'config/environments/$OSO_ENVIRONMENT'
        rescue Bundler::GemNotFound => gem_error
          puts "Error loading libs or gems: #{gem_error}"
          exit 1
        end
	Broker::Application.initialize!
        puts $1
EOF
  else
      fail ruby is not installed or not executable
  fi
}

# ============================================================================
#
# Base Packages and Components
#
# ============================================================================

# Network Manager (disabled or not present)

# network "service" enabled

# SSH max connections

# Kernel Settings
#  - Ephemeral Port Range
#  - Kernel Semaphores (httpd communications)
#  - netfilter conntrack buffer size

# IPTables

# ============================================================================
#
# Configuration and Variables
# 
# ============================================================================

#
# Check packages
#
function check_packages() {
    # $* = $PACKAGES
    verbose checking packages
    for PKGNAME in $*
    do
	verbose checking package $PKGNAME
	PKGSTATUS=`rpm -q $PKGNAME`
	if echo $PKGSTATUS | grep "not installed" >/dev/null
	then
	    fail "package $PKGNAME is not installed"
	fi 
    done
}


#
# There are two different service monitors:  RHEL6 still uses service and chkconfig
# Fedora 16+ use systemctl (as part of systemd)
#
function service_enabled() {
  # $1 = SERVICE_NAME

  if [ -x /bin/systemctl -a ! -x /etc/init.d/$1 ]
  then
      systemctl is-enabled $1.service 2>&1 >/dev/null
  else
      chkconfig $1
  fi

  if [ $? != 0 ]
  then
      fail "service $1 not enabled"
  fi
}

function service_running() {
  # $1 = SERVICE_NAME

  if [ -x /bin/systemctl ]
  then
      systemctl status $1.service 2>&1 | grep -e "^\s*Active: active" >/dev/null
  else
      service $1 status 2>&1 > /dev/null
  fi

  if [ $? != 0 ]
  then
      fail "service $1 not running"
  fi
}


#
# Check services
#
function check_services() {
    verbose checking services
    for SVCNAME in $SERVICES
    do
	service_enabled $SVCNAME
        service_running $SVCNAME
    done
}

function check_selinux_modules() {
  verbose "checking that selinux modules are loaded"
  # if not enforcing, just say so
  notice "SELinux is " `getenforce`
  if [ `getenforce 2>/dev/null` != "Enforcing" ]
  then
    return
  fi

  SEMODULE=$(which semodule 2>/dev/null)
  if [ $? != 0 ]
  then
    fail "semodule binary not present (from policycoreutils RPM)"
    return
  fi

  SELINUX_MODULES='rubygem_passenger openshift-origin-broker'
  for MODNAME in $SELINUX_MODULES
  do
    MODSPEC=$($SEMODULE -l | grep $MODNAME)
    if [ -n "$MODSPEC" ]
    then
      MODVERS=$(echo $MODSPEC | awk '{print $2;}')
      verbose "selinux module $MODNAME is version $MODVERS"
    else
      fail "module $MODNAME is not loaded"
    fi
  done
}


function check_kernel_settings() {
  verbose "checking kernel settings"

  KERNEL_SETTINGS="kernel.sem net.ipv4.ip_local_port_range net.netfilter.nf_conntrack_max"

  for KERNEL_KEY in $KERNEL_SETTINGS
  do
      notice current - $(sysctl $KERNEL_KEY)
      notice config - $(grep -e "^$KERNEL_KEY " /etc/sysctl.conf)
  done
}

function check_firewall() {
  verbose "checking firewall settings"

  
  # check for 'enabled'
  service_enabled iptables
  service_running iptables
  # Need to check for ports TCP/22, TCP/53, TCP/80, TCP/443 and UDP/53 allowed
}

#
# Check SELinux
#
function check_selinux_booleans() {
  # if not enforcing, just say so
  notice "SELinux is " `getenforce`
  if [ `getenforce 2>/dev/null` != "Enforcing" ]
  then
    return
  fi

  # check httpd_unified
  if getsebool httpd_unified | grep -e '--> on' >/dev/null
  then
    verbose SELinux boolean httpd_unified is enabled
  else
    fail SELinux boolean httpd_unified is disabled
  fi


  # Only needed with BIND DDNS plugin
  # check allow_ypbind
  # check httpd_unified
  if getsebool allow_ypbind | grep -e '--> on' >/dev/null
  then
    verbose SELinux boolean allow_ypbind is enabled
  else
    notice SELinux boolean allow_ypbind is disabled
  fi
  
}

# ============================================================================
#
# DataStore
#
# ============================================================================

function find_datastore_plugin() {
    # OpenShift::DataStore.instance_variable_get('@oo_ds_provider')
    get_application_value "OpenShift::DataStore.instance_variable_get('@oo_ds_provider')"
}

function check_datastore() {
    verbose checking datastore
    DATA_PLUGIN=`find_datastore_plugin`
    case $DATA_PLUGIN in
        'OpenShift::DataStore')

	    fail abstract datastore class: $DATA_PLUGIN
	    ;;
	'OpenShift::MongoDataStore')
	    verbose "datastore plugin: $DATA_PLUGIN"
	    check_datastore_mongo
            ;;
	
	*)
	    fail unknown datastore class: $DATA_PLUGIN
	;;
    esac
    unset DATA_PLUGIN
}

function check_mongo_login() {
    # $HOST_PORT=$1
    # $DB=$2
    # $USER=$3
    # $PASS=$4
    verbose checking mongo db login access

    mongo $1/$2 --username $3 --password <<EOF 2>&1 >/dev/null 
$4
exit
EOF

    if [ $? -eq 0 ]
    then
	verbose "mongo db login successful: $1/$2 --username $3"
    else
        fail "error logging into mongo db: $1/$2 --username $3, exit code: $?"
    fi
}

function check_datastore_mongo() {
    verbose checking mongo datastore configuration

    # get the service hostname, port, username, password
    DS_HOST=`get_application_value "Rails.application.config.datastore[:host_port][0]"`
    DS_PORT=`get_application_value "Rails.application.config.datastore[:host_port][1]"`
    DS_USER=`get_application_value "Rails.application.config.datastore[:user]"`
    DS_PASS=`get_application_value "Rails.application.config.datastore[:password]"`
    DS_NAME=`get_application_value "Rails.application.config.datastore[:db]"`
    verbose "Datastore Host: $DS_HOST"
    verbose "Datastore Port: $DS_PORT"
    verbose "Datastore User: $DS_USER"
    if [ "$DS_PASS" == "mooo" ]
    then
	fail "Datastore Password is still the default"
    else
        verbose "Datastore Password has been reset"
    fi
    verbose "Datastore DB Name: $DS_NAME"

    # Only check local values if DS_HOST is localhost
    if [ "$DS_HOST" = "localhost" ]
    then
	verbose "Datastore configuration is on localhost"

	# check presence of mongodb package
	check_packages mongodb

        # check auth enabled
        if grep -e '^auth = true' /etc/mongodb.conf >/dev/null
        then
            verbose "LOCAL: mongod auth is enabled"
        else
	    fail "LOCAL: mongod auth is not enabled in /etc/mongodb.conf"
        fi

        # check service enabled
	if (service mongod status 2>/dev/null | grep enabled 2>&1 >/dev/null \
            || chkconfig --list 2>/dev/null | grep '^mongod.*2:on.*3:on.*4:on.*5:on' 2>&1 >/dev/null)
        then
	    verbose "LOCAL: mongod service enabled"
        else
	    fail "LOCAL: mongod service not enabled"
        fi

        # check service started
	if (service mongod status 2>/dev/null | grep 'running' 2>&1 >/dev/null)
        then
	    verbose "LOCAL: mongod service running"
        else
	    fail "LOCAL: mongod service not running"
        fi
    else
        verbose "Datastore: mongo db service is remote"
    fi

    # check OpenShift Origin user (username/password)
    check_mongo_login $DS_HOST:$DS_PORT $DS_NAME $DS_USER $DS_PASS
}

function check_auth_mongo() {
    verbose checking mongo auth configuration

    
    # get the service hostname, port, username, password
    DS_HOST=`get_application_value "Rails.application.config.auth[:mongo_host_port][0]"`
    DS_PORT=`get_application_value "Rails.application.config.auth[:mongo_host_port][1]"`
    DS_USER=`get_application_value "Rails.application.config.auth[:mongo_user]"`
    DS_PASS=`get_application_value "Rails.application.config.auth[:mongo_password]"`
    DS_NAME=`get_application_value "Rails.application.config.auth[:mongo_db]"`
    verbose "Auth Host: $DS_HOST"
    verbose "Auth Port: $DS_PORT"
    verbose "Auth User: $DS_USER"
    if [ "$DS_PASS" == "mooo" ]
    then
	fail "Datastore Password is still the default"
    else
        verbose "Datastore Password has been reset"
    fi
    verbose "Auth DB Name: $DS_NAME"

    # Only check local values if DS_HOST is localhost
    if [ "$DS_HOST" = "localhost" ]
    then
	verbose "Auth configuration is on localhost"

	# check presence of mongodb package
	check_packages mongodb

        # check auth enabled
        if grep -e '^auth = true' /etc/mongodb.conf >/dev/null
        then
            verbose "LOCAL: mongod auth is enabled"
        else
	    fail "LOCAL: mongod auth is not enabled in /etc/mongodb.conf"
        fi

        # check service enabled
	if (service mongod status 2>/dev/null | grep enabled 2>&1 >/dev/null \
            || chkconfig --list 2>/dev/null | grep '^mongod.*2:on.*3:on.*4:on.*5:on' 2>&1 >/dev/null)
        then
	    verbose "LOCAL: mongod service enabled"
        else
	    fail "LOCAL: mongod service not enabled"
        fi

        # check service started
	if (service mongod status 2>/dev/null | grep 'running' 2>&1 >/dev/null)
        then
	    verbose "LOCAL: mongod service running"
        else
	    fail "LOCAL: mongod service not running"
        fi
    else
        verbose "Auth: mongo db service is remote"
    fi

    # check OpenShift Origin user (username/password)
    check_mongo_login $DS_HOST:$DS_PORT $DS_NAME $DS_USER $DS_PASS
}

function get_http_response_code() {
    curl -k -s -o /dev/null -w '%{http_code}'  "$1"
}

assert_rest_api_returns() {
    if [ "$1" = "$(get_http_response_code "$2")" ]
    then
        verbose "Got HTTP $1 response from $2"
    else
        fail "Did not get expected HTTP $1 response from $2"
    fi
}

function check_auth_remote_user() {
    verbose checking remote-user auth configuration

    RU_TRUSTED_HEADER=`get_application_value "Rails.application.config.auth[:trusted_header]"`
    verbose "Auth trusted header: $RU_TRUSTED_HEADER"

    if grep -q 'BrowserMatchNoCase\s\+\^OpenShift\s\+passthrough' ${OSO_HTTPD_CONF_DIR}/*.conf \
       && grep -q 'Allow\s\+from\s\+env=passthrough' ${OSO_HTTPD_CONF_DIR}/*.conf
    then
        verbose 'Auth passthrough is enabled for OpenShift services'
    else
        fail 'Auth passthrough appears not to be enabled, which will break JBossTools and node-to-broker authentication'
    fi

    UNAUTHENTICATED_APIS="api application_templates cartridges"
    AUTHENTICATED_APIS="user domains"

    for api in $UNAUTHENTICATED_APIS
    do
        assert_rest_api_returns 200 "${BROKER_REST_API_BASE}${api}"
    done

    for api in $AUTHENTICATED_APIS
    do
        assert_rest_api_returns 401 "${BROKER_REST_API_BASE}${api}"
    done
}
# ============================================================================
#
# Cloud User Authentication
#
# ============================================================================

function find_authentication_plugin() {
    # OpenShift::AuthService.instance_variable_get('@oo_auth_provider')
    get_application_value "OpenShift::AuthService.instance_variable_get('@oo_auth_provider')"

}

function check_authentication() {
    verbose checking cloud user authentication
    AUTH_PLUGIN=`find_authentication_plugin`
    verbose auth plugin = $AUTH_PLUGIN
    case $AUTH_PLUGIN in
        'OpenShift::AuthService')

	    fail abstract auth class: $AUTH_PLUGIN
	    ;;
	'OpenShift::MongoAuthService')
	    verbose "auth plugin: $AUTH_PLUGIN"
	    check_auth_mongo
            ;;
	
	'OpenShift::RemoteUserAuthService')
	    verbose "auth plugin: $AUTH_PLUGIN"
	    check_auth_remote_user
            ;;

	*)
	    fail unknown auth class: $AUTH_PLUGIN
	    ;;
    esac
    unset AUTH_PLUGIN
}

# ============================================================================
#
# Dynamic DNS Updates
#
# ============================================================================

function find_dynamic_dns_plugin() {
    # OpenShift::DnsService.instance_variable_get('@oo_dns_provider')
    get_application_value "OpenShift::DnsService.instance_variable_get('@oo_dns_provider')"
}

function dns_bind_update_record() {
    # $1 = server
    # $2 = key name
    # $3 = key value
    # $4 = function (add|delete)
    # $5 = type (A, TXT, CNAME)
    # $6 = name 
    # $7 = value

    # check $1: should be an IP address
    # check $3: should be a key string
    # check $4: should be add|delete
    # check $5 should be A, TXT, CNAME

    verbose "${4}ing $5 record named $6 to server $1: $7"

    nsupdate <<EOF
server $1
key $2 $3
update $4 $6 1 $5 $7
send
EOF

    if [ $? != 0 ]
    then
	fail "error ${4}ing $5 record name $6 to server $1: $7"
    fi

}

function check_dns_bind() {
    verbose checking bind dns plugin configuration

    # host
    # port
    # keyname
    # keyvalue
    # zone
    # domain_suffix

    DNS_SERVER=`get_application_value "Rails.application.config.dns[:server]"`
    DNS_PORT=`get_application_value "Rails.application.config.dns[:port].to_s"`
    DNS_KEYNAME=`get_application_value "Rails.application.config.dns[:keyname]"`
    DNS_KEYVAL=`get_application_value "Rails.application.config.dns[:keyvalue]"`
    DNS_ZONE=`get_application_value "Rails.application.config.dns[:zone]"`
    DNS_SUFFIX=`get_application_value "Rails.application.config.openshift[:domain_suffix]"`
    verbose "DNS Server: $DNS_SERVER"
    verbose "DNS Port: $DNS_PORT"
    verbose "DNS Key Name: $DNS_KEYNAME"
    verbose "DNS Key Value: *****"
    verbose "DNS Zone: $DNS_ZONE"
    verbose "DNS Domain Suffix: $DNS_SUFFIX"
    
    # check that zone suffix ends exactly with dns_zone (zone contains suffix)

    # try to add a dummy TXT record to the zone
    dns_bind_update_record $DNS_SERVER $DNS_KEYNAME $DNS_KEYVAL add txt testrecord.$DNS_SUFFIX this_is_a_test

    # verify that the record is there
    if host -t txt testrecord.$DNS_SUFFIX $DNS_SERVER >/dev/null
    then
	verbose "txt record successfully added"
    else
        fail "txt record testrecord.$DNS_SUFFIX does not resolve on server $DNS_SERVER"
    fi

    # remove it.
    dns_bind_update_record $DNS_SERVER $DNS_KEYNAME $DNS_KEYVAL delete txt testrecord.$DNS_SUFFIX

    # verify that the record is removed
    if host -t txt testrecord.$DNS_SUFFIX $DNS_SERVER >/dev/null
    then
        fail "txt record testrecord.$DNS_SUFFIX still resolves on server $DNS_SERVER"
    else
	verbose "txt record successfully deleted"
    fi
}

function check_dynamic_dns() {
    verbose checking dynamic dns plugin
    NAME_PLUGIN=`find_dynamic_dns_plugin`
    case $NAME_PLUGIN in
        'OpenShift::DnsService')
	    fail abstract dns class: $NAME_PLUGIN
	    ;;

	    
	'OpenShift::BindPlugin')
	    verbose dynamic dns plugin = $NAME_PLUGIN
	    check_dns_bind
	    ;;

	*)
	    fail unknown dns class: $NAME_PLUGIN
	    ;;
    esac
    unset NAME_PLUGIN
}

# ============================================================================
#
# Broker -> Node messaging
#
# ============================================================================
function find_messaging_plugin() {
    get_application_value "OpenShift::ApplicationContainerProxy.instance_variable_get('@proxy_provider')"
}

function check_messaging() {
    verbose checking messaging configuration

    MSG_PLUGIN=`find_messaging_plugin`
    case $MSG_PLUGIN in
        'OpenShift::ApplicationContainerProxy')
	    fail abstract messaging class: $MSG_PLUGIN
	    ;;

	    
	'OpenShift::MCollectiveApplicationContainerProxy')
	    verbose messaging plugin = $MSG_PLUGIN
	    ;;

	*)
	    fail unknown messaging class: $MSG_PLUGIN
	    ;;
    esac
    unset MSG_PLUGIN
}

# ==========================================================================
# Process CLI arguments
# ==========================================================================

function print_help() {
    echo "usage: $0 [-h] [-v]

  -h) help (print this message)
  -v) verbose - notify when each check begins
"
    exit 0
}


OPT_FORMAT="dhvS:A:D:I:a:"

while getopts $OPT_FORMAT OPTION
do
    case $OPTION in
        d) 
	    set -x
            ;;

	h) 
	    print_help
	    ;;

	v)
	    VERBOSE="true"
	    ;;


	# storage
	S)
	    DATA_SERVICE=$OPTION
	    ;;

	# auth
	A)
	    AUTH_SERVICE=$OPTION
	    ;;

	# DNS
	D)
	    NAME_SERVICE=$OPTION
	    ;;

	# Interface
        I)
	    INTERFACE=$OPTION
	    ;;

	# IP Address
        a)
	    IP_ADDRESS=$OPTION
	    ;;

        ?) print_help
        ;;
    esac
done

# Set defaults if not provided
DATA_SERVICE=${DATA_SERVICE:=${DEFAULT_DATA_SERVICE}}
AUTH_SERVICE=${AUTH_SERVICE:=${DEFAULT_AUTH_SERVICE}}
NAME_SERVICE=${NAME_SERVICE:=${DEFAULT_NAME_SERVICE}}
# INTERFACE=${INTERFACE:=$(guess_interface)}
# IP_ADDRESS=${IP_ADDRESS:=$(guess_ip_address)}

verbose SERVICES: DATA: $DATA_SERVICE, Auth: $AUTH_SERVICE, Name $NAME_SERVICE

AUTH_MODULE=`echo $AUTH_MODULE_PATTERN | sed -e "s/%NAME%/$AUTH_SERVICE/"`
NAME_MODULE=`echo $NAME_MODULE_PATTERN | sed -e "s/%NAME%/$NAME_SERVICE/"`

verbose AUTH_MODULE: $AUTH_MODULE
verbose NAME_MODULE: $NAME_MODULE

# =============================================================================
#
# MAIN
#
# =============================================================================

# Initial status is PASS (0)
# each fail adds one
STATUS=0

probe_version

check_packages $PACKAGES
check_ruby_requirements $OSO_RUBY_LIBS
#check_selinux_modules
#check_selinux_booleans
#check_kernel_settings
check_firewall
check_services

check_datastore
check_authentication
check_dynamic_dns
check_messaging

if [ "$STATUS" -eq 0 ]
then
    echo PASS
else
    echo $STATUS ERRORS
fi
exit $STATUS
