#!/bin/bash
#
# Stand-alone LDAP Daemon (slapd)
#
# Description:  Manages Stand-alone LDAP Daemon (slapd) as an OCF resource in
#               an high-availability setup.
#
# Authors:      Jeroen Koekkoek
#               nozawat@gmail.com
#               John Keith Hohm
#
# License:      GNU General Public License (GPL)
# Copyright:    (C) 2011 Pagelink B.V.
#
#       The OCF code was inspired by the Postfix resource script written by
#       Raoul Bhatia <r.bhatia@ipax.at>.
#
#       The code for managing the slapd instance is based on the the slapd init
#       script found in Debian GNU/Linux 6.0.
#
# OCF parameters:
#   OCF_RESKEY_slapd
#   OCF_RESKEY_ldapsearch
#   OCF_RESKEY_config
#   OCF_RESKEY_pidfile
#   OCF_RESKEY_user
#   OCF_RESKEY_group
#   OCF_RESKEY_services
#   OCF_RESKEY_watch_suffix
#   OCF_RESKEY_ignore_suffix
#   OCF_RESKEY_bind_dn
#   OCF_RESKEY_password
#   OCF_RESKEY_parameters
#   OCF_RESKEY_stop_escalate
#
################################################################################

# Initialization:

: ${OCF_FUNCTIONS_DIR=${OCF_ROOT}/resource.d/heartbeat}
. ${OCF_FUNCTIONS_DIR}/.ocf-shellfuncs

: ${OCF_RESKEY_slapd="/usr/sbin/slapd"}
: ${OCF_RESKEY_ldapsearch="ldapsearch"}
: ${OCF_RESKEY_config=""}
: ${OCF_RESKEY_pidfile=""}
: ${OCF_RESKEY_user=""}
: ${OCF_RESKEY_group=""}
: ${OCF_RESKEY_services="ldap:///"}
: ${OCF_RESKEY_watch_suffix=""}
: ${OCF_RESKEY_ignore_suffix=""}
: ${OCF_RESKEY_bind_dn=""}
: ${OCF_RESKEY_password=""}
: ${OCF_RESKEY_parameters=""}
: ${OCF_RESKEY_stop_escalate=30}

USAGE="Usage: $0 {start|stop|status|monitor|validate-all|meta-data}"
ORIG_IFS=$IFS
NEWLINE='
'

################################################################################

usage() {
    echo $USAGE >&2
}

meta_data()
{
  cat <<END
<?xml version="1.0"?>
<!DOCTYPE resource-agent SYSTEM "ra-api-1.dtd">
<resource-agent name="slapd">
<version>0.1</version>

<longdesc lang="en">
Resource script for Stand-alone LDAP Daemon (slapd). It manages a slapd instance as an OCF resource.
</longdesc>
<shortdesc lang="en">Manages a Stand-alone LDAP Daemon (slapd) instance</shortdesc>

<parameters>

<parameter name="slapd" unique="0" required="0">
<longdesc lang="en">
Full path to the slapd binary.
For example, "/usr/sbin/slapd".
</longdesc>
<shortdesc lang="en">Full path to slapd binary</shortdesc>
<content type="string" default="/usr/sbin/slapd" />
</parameter>

<parameter name="ldapsearch" unique="0" required="0">
<longdesc lang="en">
Full path to the ldapsearch binary.
For example, "/usr/bin/ldapsearch".
</longdesc>
<shortdesc lang="en">Full path to ldapsearch binary</shortdesc>
<content type="string" default="ldapsearch" />
</parameter>

<parameter name="config" required="0" unique="1">
<longdesc lang="en">
Full path to a slapd configuration directory or a slapd configuration file.
For example, "/etc/ldap/slapd.d" or "/etc/ldap/slapd.conf".
</longdesc>
<shortdesc>Full path to configuration directory or file</shortdesc>
<content type="string" default=""/>
</parameter>

<parameter name="pidfile" required="0" unique="0">
<longdesc lang="en">
File to read the PID from; read from olcPidFile/pidfile in config if not set.
</longdesc>
<shortdesc lang="en">File to read PID from</shortdesc>
<content type="string" default="" />
</parameter>

<parameter name="user" unique="0" required="0">
<longdesc lang="en">
User name or id slapd will run with. The group id is also changed to this
user's gid, unless the group parameter is used to override.
</longdesc>
<shortdesc lang="en">User name or id slapd will run with</shortdesc>
<content type="string" default="" />
</parameter>

<parameter name="group" unique="0" required="0">
<longdesc lang="en">
Group name or id slapd will run with.
</longdesc>
<shortdesc lang="en">Group name or id slapd will run with</shortdesc>
<content type="string" default="" />
</parameter>

<parameter name="services" required="0" unique="1">
<longdesc lang="en">
LDAP (and other scheme) URLs slapd will serve.
For example, "ldap://127.0.0.1:389 ldaps:/// ldapi:///"
</longdesc>
<shortdesc>LDAP (and other scheme) URLs to serve</shortdesc>
<content type="string" default="ldap:///"/>
</parameter>

<parameter name="watch_suffix" required="0" unique="0">
<longdesc lang="en">
Suffix (database backend) that will be monitored for availability. Multiple
suffixes can be specified by providing a space seperated list. By providing one
or more suffixes here, the ignore_suffix parameter is discarded. All suffixes
will be monitored if left blank.
</longdesc>
<shortdesc>Suffix that will be monitored for availability.</shortdesc>
<content type="string" default=""/>
</parameter>

<parameter name="ignore_suffix" required="0" unique="0">
<longdesc lang="en">
Suffix (database backend) that will not be monitored for availability. Multiple
suffixes can be specified by providing a space seperated list. No suffix will
be excluded if left blank.
</longdesc>
<shortdesc>Suffix that will not be monitored for availability.</shortdesc>
<content type="string" default=""/>
</parameter>

<parameter name="bind_dn" required="0" unique="0">
<longdesc lang="en">
Distinguished Name used to bind to the LDAP directory for testing. Leave blank
to bind to the LDAP directory anonymously.
</longdesc>
<shortdesc>Distinguished Name used to bind to the LDAP directory for testing.</shortdesc>
<content type="string" default=""/>
</parameter>

<parameter name="password" required="0" unique="0">
<longdesc lang="en">
Password used to bind to the LDAP directory for testing.
</longdesc>
<shortdesc>Password used to bind to the LDAP directory for testing.</shortdesc>
<content type="string" default=""/>
</parameter>

<parameter name="parameters" unique="0" required="0">
<longdesc lang="en">
slapd may be called with additional parameters.
Specify any of them here.
</longdesc>
<shortdesc lang="en">Any additional parameters to slapd.</shortdesc>
<content type="string" default="" />
</parameter>

<parameter name="stop_escalate" unique="0" required="0">
<longdesc lang="en">
Number of seconds to wait for shutdown (using SIGTERM) before resorting to
SIGKILL
</longdesc>
<shortdesc lang="en">Seconds before stop escalation to KILL</shortdesc>
<content type="integer" default="30" />
</parameter>
</parameters>

<actions>
<action name="start"   timeout="20s" />
<action name="stop"    timeout="20s" />
<action name="monitor" depth="0"  timeout="20s" interval="60s" />
<action name="validate-all"  timeout="20s" />
<action name="meta-data"  timeout="5s" />
</actions>
</resource-agent>
END
}

terminate()
{
  local pid=$1
  local signal=$2
  local recheck=${3-0}
  local result
  local waited=0

  kill -$signal $pid >/dev/null 2>&1; result=$?

  while [ $result -eq 0 ] && [ $recheck -eq 0 ] || [ $waited -lt $recheck ]; do
    kill -0 $pid >/dev/null 2>&1; result=$?
    let "waited += 1"

    if [ $result -eq 0 ]; then
      sleep 1
    fi
  done

  if [ $result -ne 0 ]; then
    return 0
  fi

  return 1
}

watch_suffix()
{
  local result

  if [ -n "$OCF_RESKEY_watch_suffix" ]; then
    if echo "'$OCF_RESKEY_watch_suffix'" | grep "'$1'" >/dev/null 2>&1; then
      result=0
    else
      result=1
    fi
  else
    if echo "'$OCF_RESKEY_ignore_suffix'" | grep "'$1'" >/dev/null 2>&1; then
      result=1
    else
      result=0
    fi
  fi

  return $result
}

slapd_pid()
{
  local pid

  if [ -f "$pid_file" ]; then
    pid=`head -n 1 "$pid_file" 2>/dev/null`

    if [ "X$pid" != "X" ]; then
      echo "$pid"
      return $OCF_SUCCESS
    fi

    ocf_log err "slapd pid file '$pid_file' empty."
    return $OCF_ERR_GENERIC
  fi

  ocf_log info "slapd pid file '$pid_file' does not exist."
  return $OCF_NOT_RUNNING
}

slapd_status()
{
  local pid=$1
  local state=$?

  if [ $state -eq $OCF_SUCCESS ]; then

    if ! kill -0 $pid >/dev/null 2>&1; then
      return $OCF_NOT_RUNNING
    else
      return $OCF_SUCCESS
    fi
  fi

  return $state
}

slapd_start()
{
  local options
  local reason
  local result
  local state

  slapd_status `slapd_pid`; state=$?

  if [ $state -eq $OCF_SUCCESS ]; then
    ocf_log info "slapd already running."
    return $state
  elif [ $state -eq $OCF_ERR_GENERIC ]; then
    return $state
  fi

  options="-u $user -g $group"

  if [ -d "$config" ]; then
    options="$options -F $config"
  elif [ -f "$config" ]; then
    options="$options -f $config"
  else
    ocf_log err "slapd configuration '$config' does not exist."
    return $OCF_ERR_INSTALLED
  fi

  if [ -n "$parameters" ]; then
    options="$options $parameters"
  fi

  if [ -n "$services" ]; then
    $slapd -h "$services" $options 2>&1; result=$?
  else
    $slapd $options 2>&1; result=$?
  fi

  if [ $result -ne 0 ]; then
    ocf_log err "slapd returned error."

    return $OCF_ERR_GENERIC
  fi

  ocf_log info "slapd started."

  return $OCF_SUCCESS
}

slapd_stop()
{
  local pid
  local result
  local state

  pid=`slapd_pid`; slapd_status $pid; state=$?

  if [ $state -eq $OCF_NOT_RUNNING ]; then
    ocf_log info "slapd already stopped."
    return $OCF_SUCCESS
  elif [ $state -eq $OCF_ERR_GENERIC ]; then
    return $state
  fi

  terminate $pid TERM $OCF_RESKEY_stop_escalate; result=$?
  if [ $result -ne 0  ]; then
    ocf_log err "slapd failed to stop. Escalating to KILL."
    terminate $pid KILL; result=$?
  fi

  if [ -f "$pid_file" ]; then
    rm -f "$pid_file" >/dev/null 2>&1
  fi

  ocf_log info "slapd stopped."
  return $OCF_SUCCESS
}

slapd_monitor()
{
  local options
  local result
  local state
  local suffix
  local suffixes

  slapd_status `slapd_pid`; state=$?
  if [ $state -eq $OCF_NOT_RUNNING ]; then
    ocf_log debug "slapd is stopped."
    return $state
  elif [ $state -ne $OCF_SUCCESS ]; then
    ocf_log err "slapd returned error."
    return $state
  fi

  if [ -d "$config" ]; then
    for suffix in `find "$config"/'cn=config' -type f -name olcDatabase* -exec \
                   sed -ne 's/^[[:space:]]*olcSuffix:[[:space:]]\+\(.\+\)/\1/p' {} \;`
    do
      suffix=${suffix#\"*}
      suffix=${suffix%\"*}

      if watch_suffix $suffix; then
        suffixes="$suffixes $suffix"
      fi
    done

  elif [ -f "$config" ]; then
    for suffix in `sed -ne 's/^[[:space:]]*suffix[[:space:]]\+\(.\+\)/\1/p' "$config"`
    do
      suffix=${suffix#\"*}
      suffix=${suffix%\"*}

      if watch_suffix $suffix; then
        suffixes="$suffixes $suffix"
      fi
    done

  else
    ocf_log err "slapd configuration '$config' does not exist."
    return $OCF_ERR_INSTALLED
  fi

  options="-LLL -s base -x"

  if [ -n "$bind_dn" ]; then
    options="$options -D '$bind_dn' -w '$password'"
  fi

  for suffix in $suffixes; do
    ocf_run "$ldapsearch" -H "$services" -b "$suffix" $options >/dev/null 2>&1; result=$?

    case "$result" in
      "0")
        ocf_log debug "slapd database with suffix '$suffix' reachable"
        ;;
      "49")
        ocf_log err "slapd database with suffix '$suffix' unreachable. Invalid credentials."
        return $OCF_ERR_CONFIGURED
        ;;
      *)
        ocf_log err "slapd database with suffix '$suffix' unreachable."
        state=$OCF_ERR_GENERIC
        ;;
    esac
  done

  return $state
}

slapd_validate_all()
{
  check_binary "$slapd"
  check_binary "$ldapsearch"

  if [ -z "$pid_file" ]; then
    if [ -d "$config" ]; then
      pid_file=`sed -ne \
	       's/^olcPidFile:[[:space:]]\+\(.\+\)[[:space:]]*/\1/p' \
	       "$config"/'cn=config.ldif' 2>/dev/null`
    elif [ -f "$config" ]; then
      pid_file=`sed -ne \
		's/^pidfile[[:space:]]\+\(.\+\)/\1/p' \
		"$config" 2>/dev/null`
    else
      ocf_log err "slapd configuration '$config' does not exist."
      return $OCF_ERR_INSTALLED
    fi
  fi

  if [ -z "$user" ]; then
    user=`id -nu 2>/dev/null`
  elif ! id "$user" >/dev/null 2>&1; then
    ocf_log err "slapd user '$user' does not exist"
    return $OCF_ERR_INSTALLED
  fi

  if [ -z "$group" ]; then
    group=`id -ng 2>/dev/null`
  elif ! grep "^$group:" /etc/group >/dev/null 2>&1; then
    ocf_log err "slapd group '$group' does not exist"
    return $OCF_ERR_INSTALED
  fi

  return $OCF_SUCCESS
}

#
# Main
#

slapd=$OCF_RESKEY_slapd
ldapsearch=$OCF_RESKEY_ldapsearch
config=$OCF_RESKEY_config
user=$OCF_RESKEY_user
group=$OCF_RESKEY_group
services=$OCF_RESKEY_services
bind_dn=$OCF_RESKEY_bind_dn
password=$OCF_RESKEY_password
parameters=$OCF_RESKEY_parameters
pid_file=$OCF_RESKEY_pidfile

if [ -z "$config" ]; then
  if [ -e "/etc/ldap/slapd.d" ]; then
    config="/etc/ldap/slapd.d"
  else
    config="/etc/ldap/slapd.conf"
  fi
fi

if [ $# -ne 1 ]; then
  usage
  exit $OCF_ERR_ARGS
fi

case $1 in
  meta-data)
    meta_data
    exit $OCF_SUCCESS
    ;;
  usage|help)
    usage
    exit $OCF_SUCCESS
    ;;
esac

slapd_validate_all

[ $? -eq $OCF_SUCCESS ] || exit $?

case $1 in
  status)
    slapd_status `slapd_pid`; state=$?

    if [ $state -eq $OCF_SUCCESS ]; then
      ocf_log debug "slapd is running."
    elif [ $state -eq $OCF_NOT_RUNNING ]; then
      ocf_log debug "slapd is stopped."
    fi

    exit $state
    ;;
  start)
    slapd_start
    exit $?
    ;;
  stop)
    slapd_stop
    exit $?
    ;;
  monitor)
    slapd_monitor; state=$?
    exit $state
    ;;
  validate-all)
    exit $OCF_SUCCESS
    ;;
  *)
    usage
    exit $OCF_ERR_UNIMPLEMENTED
    ;;
esac