#!/bin/bash
# ======================================================================
#
# director helper
#
#   C
#   R
#   U
#   D
#     actions for a host, commands, services
#
# This script contains specific logic for IML
#   - use puppet facts
#   - UniBe network and group names - see hostCreate()
#
# ----------------------------------------------------------------------
# ah = axel.hahn@iml.unibe.ch
# 2022-02-16  v0.2  ah  add --cfg param
# 2022-03-04  v0.3  ah  abort on http 5xx error
# ======================================================================

tmpfile=/tmp/outcurl.tmp
tmpfile2=/tmp/outcurl2.tmp

# a custom file to source instead of detecting local data
loadfile=

# UNUSED
# APICLIENT=`dirname $0`/api2director


# MY_NAME=`facter fqdn | cut -f -2 -d ">"`
# MY_IP=`facter ipaddress | cut -f -2 -d ">"`
# TODO: test ... maybe uncomment it again
# MY_NAME=`hostname -f`
# MY_IP=`_getIpFrontend`


typeset cfg_debug=false
typeset cfg_dryrun=false

# ======================================================================
#
# FUNCTIONS
#
# ======================================================================


  # helper to make http base setup for host actions
  function _initHttp(){
    # see inc_functions
    _initHttpWithConfigfile "${dir_cfg}/api-director.cfg"
    if [ $cfg_debug = true ]; then
      http.setDebug 1
    fi
  }

  function _initVars(){
    if [ -z "$loadfile" ]; then
      MY_NAME=`hostname -f`
      MY_IP=`_getIpFrontend | head -1`
      MY_ZONE=
    fi
    IDC_host__cachefile="${dir_data}/${MY_NAME}__host_at-director.txt"
    IDC_service__cachefile="${dir_data}/${MY_NAME}__all_defined_services__at-director.txt"
    IDC_svcathost__cachefile="${dir_data}/${MY_NAME}__services_on_host__at-director.txt"
    # ch="`dirname $0`/inc/confighandler.sh"
    ch="./inc/confighandler.sh"
  }

  function flushDatadir(){
    if [ ! -z "${dir_data}" -a -d "${dir_data}" ]; then
      _wd "deleting ${dir_data} ..."
      rm -f ${dir_data}/*.txt
    fi
  }

# ----------------------------------------------------------------------
# functions for objects
# ----------------------------------------------------------------------

  # ............................................................
  # set $ch to store all object vars

  function _generateJsonForHost(){

    if [ -z "$loadfile" ]; then
      # --- host infos

      local MY_Platform=`uname -a | cut -f 1 -d ' '`
      . `dirname $0`/plugins/inc_pluginfunctions || exit 1
      local MY_OSName=`ph.getOS`
      local MY_OSMajorVersion=`ph.getOSMajor`
    fi

    export CFGSTORAGE="directorhost"
    (
      $ch --flush
      $ch --set  object_name     \"$MY_NAME\"
      $ch --set  object_type     '"object"'
      $ch --set  address         \"$MY_IP\"
      $ch --set  display_name    \"$MY_NAME\"
      $ch --set  zone            \"$MY_ZONE\"

      $ch --set  icon_image      \"/images/os/${MY_OSName}.png\"
      $ch --set  icon_image_alt  \"${MY_Platform}\:\ ${MY_OSName}\ ${MY_OSMajorVersion}\"
    ) 2>/dev/null 

    # --- detect phase
    # local phase="live"
    # for myphase in preview stage demo
    # do
    #   echo $MY_NAME | grep "\.$myphase\." >/dev/null && phase=$myphase
    # done

    (
      # ----- facter data to host vars
      $ch --set  vars.platform  \"${MY_Platform}\"
      $ch --set  vars.os        \"${MY_OSName}${MY_OSMajorVersion}\"

      # ----- set host type

      # host in the UNIBE network:
      echo $MY_IP | grep -E "^(10\.|172\.1[6-9]\.|172.2[0-9]\.|172\.3[01]\.|192.168\.)" >/dev/null
      if [ $? -eq 0 ]; then
        $ch --set  imports  '["host passive only"]'
      else
        $ch --set  imports  '["host in network"]'

        # port checks initiated by icinga server to monitor client
        if [ ! -z "${host_vars_tcpport}" -a "${host_vars_tcpport}" != "[]" ]; then
          host_vars_tcpport=`echo ${host_vars_tcpport} | sed "s# ##g"`
          $ch --set  vars.tcp_port  ${host_vars_tcpport}
        fi
      fi


      # ----- /host type


      # ----- host groups
      #       all host groups must exist in director - otherwise the creation
      #       of a host will fail
      #       see https://icinga.one.iml.unibe.ch/icingaweb2/director/dashboard?name=hosts#!/icingaweb2/director/hostgroups

      $ch --set  groups        '["iml", "iml-server"]'


      # ... and add some others
      # $ch --add  groups        \"iml-phase-$phase\"

    ) 2>/dev/null

    # ----- generate data and send to DIRECTOR
    # DATA=`$ch --json 2>/dev/null`
    # $ch --flush 2>/dev/null
  }

  # ............................................................
  # set $ch to store all object vars
  # UNUSED see response of GET director/commands/templates
  # function _generateJsonForCommand(){
  #   export CFGSTORAGE="command-${IDC_command__obj_name}"
  #   (
  #     $ch --flush
  #     $ch --set  object_name     "\"${IDC_command__obj_name}\""
  #     $ch --set  object_type     \"template\"
  # 
  #   ) 2>/dev/null
  # }

  # ............................................................
  # set $ch to store all object vars
  function _generateJsonForServicetemplate(){
    export CFGSTORAGE="service-${IDC_service__obj_name}"
    (
      $ch --flush
      $ch --set  object_name     "\"${IDC_service__obj_name}\""
      $ch --set  check_command   \"${IDC_command__obj_name}\"
      $ch --set  object_type     \"template\"

      $ch --set  enable_active_checks  false
      $ch --set  enable_passive_checks true

      $ch --set  check_interval        "\"${checkInterval}s\""  # see _parseCheckConfig FILE

      # graphite
      # $ch --set  enable_perfdata       true 
      # $ch --set  vars.check_command    \"${checkName}\"         # for graphite plugin

      if [ ! -z "${checkIcon}" ]; then
        $ch --set  icon_image      \"${checkIcon}\"
        $ch --set  icon_image_alt  \"${checkName}\"
      fi

      if [ ! -z "${checkMaxAttempts}" ]; then
        $ch --set  max_check_attempts  \"${checkMaxAttempts}\"
      fi
      

    ) 2>/dev/null
  }

  # ............................................................
  # set $ch to store all object vars
  function _generateJsonForSvclink(){
    export CFGSTORAGE="servicelink-${IDC_svcathost__obj_name}"
    (
      $ch --flush
      $ch --set  object_name     "\"${IDC_svcathost__obj_name}\""
      $ch --set  object_type     \"object\"
      $ch --set  host            "\"${MY_NAME}\""
      $ch --set  imports         "[ \"${IDC_service__obj_name}\" ]"
    ) 2>/dev/null
  }

  # ............................................................
  # CRUD actions for an director object: host, service template, linked service
  # examples:
  #   ObjAction create host
  #   ObjAction list svclink
  #
  # param  string  name of action; one of create|read|update|delete|exists|list
  # param  string  name of object; one of host|service|svclink
  # param  string  Dryrun (set any not empty value to show infos without execution)
  function ObjAction(){

    local _paramAction=$1
    local _paramObj=$2
    local _paramDryrun=$cfg_dryrun
    test -z "$3" || _paramDryrun=true

    local _object_name=         # name of the current object (for ouput only)

    local _sMethod=             # http method; GET|POST|PUT|DELETE
    local _sUrl=                # relative url (part behind REST API base url)
    local _jsondata=

    local _bNeedsCheck=false    # requires $IDC* vars (a read check config first)
    local _bSendData=false      # true for PUT and POST

    local _sUrlList=
    local _sUrlCreate=
    local _sUrlExists=
    local _sUrlRead=
    local _sUrlUpdate=
    local _sUrlDelete=

    local _existFilter=

    local _bShowResponse=true
    local _bShowFilter=false
    local _bStopOnError=true
    
    # local _sCachefile=

    _initHttp

    # --- init object based vars
    case "${_paramObj}" in

      'host')
        _object_name=$MY_NAME
        _sUrlCreate=director/host
        _sUrlExists=director/host?name=${_object_name}
        _sUrlRead=director/host?name=${_object_name}
        _sUrlUpdate=director/host?name=${_object_name}
        _sUrlDelete=director/host?name=${_object_name}

        _existFilter="object_name"
        _generateJsonForHost
        ;;

      'service')
        _object_name=${IDC_service__obj_name}
        _bNeedsCheck=true

        _sUrlList=director/services/templates
        _sUrlCreate=director/service
        _sUrlExists=director/service?name=${_object_name}
        _sUrlRead=director/service?name=${_object_name}
        _sUrlUpdate=director/service?name=${_object_name}
        _sUrlDelete=director/service?name=${_object_name}

        _existFilter='"object_name": '
        _generateJsonForServicetemplate
        ;;

      'svclink')
        _object_name=${IDC_svcathost__obj_name}
        _bNeedsCheck=true
        _sUrlList=director/services?host=$MY_NAME
        _sUrlCreate=director/service
        _sUrlExists="director/service?name=${IDC_svcathost__obj_name}&host=$MY_NAME"
        _sUrlRead="director/service?name=${IDC_svcathost__obj_name}&host=$MY_NAME"
        _sUrlUpdate="director/service?name=${IDC_svcathost__obj_name}&host=$MY_NAME"
        _sUrlDelete="director/service?name=${IDC_svcathost__obj_name}&host=$MY_NAME"

        # exists:
        # _existFilter="object_name.*${IDC_svcathost__obj_name}"
        _existFilter='"object_name": '

        _generateJsonForSvclink
        ;;

      *)
        echo "ERROR: object [${_paramObj}] cannot be handled in ${FUNCNAME[0]} (yet?)."
        exit 1
    esac


    # --- init method based vars
    case "${_paramAction}" in
      'list')
        _sMethod=GET
        _sUrl=$_sUrlList
        _bShowResponse=false
        _bShowFilter=true

        _bNeedsCheck=false # 
        ;;
      'exists')
        _sMethod=GET
        _sUrl=$_sUrlExists
        _bShowResponse=false
        _bStopOnError=false
        ;;
      'create')
        _sMethod=PUT
        _sUrl=$_sUrlCreate
        _bSendData=true
        ;;
      'read')
        _sMethod=GET
        _sUrl=$_sUrlRead
        ;;
      'update')
        _sMethod=POST
        _sUrl=$_sUrlUpdate
        _bSendData=true
        ;;
      'delete')
        _sMethod=DELETE
        _sUrl=$_sUrlDelete
        ;;
      *)
        echo "ERROR: method [${_paramAction}] does not exist."
        exit 1
    esac

    if [ -z "$_sUrl" ]; then
      echo "SKIP: Action [${_paramAction}] is not supported (yet?) for object type [${_paramObj}]"
      return
    fi

    if [ $_bNeedsCheck = true -a -z "${IDC_svcathost__obj_name}" ]; then
      echo "SKIP: you need to load a check before accessing [${_paramObj}]"
      return
    fi


    # --- get json data of object
    if [ $_bSendData = true ]; then
      _jsondata=`$ch --json 2>/dev/null`
    fi
    $ch --flush 2>/dev/null

    # --- http request
    if [ ${_paramDryrun} = false ]; then
      _wd ">>>>> $_paramAction $_paramObj [${_object_name}] >> $_sMethod $_sUrl $_jsondata"
      http.makeRequest "$_sMethod" "$_sUrl" "$_jsondata"
      if _bStopOnError && http.isServerError >/dev/null; then
        echo "CRITICAL ERROR: Director API request failed with a server error $_sMethod $_sUrl"
        exit 1
      fi
      if [ $_bShowResponse = true ]; then
        http.getResponseHeader
        http.getResponse
      fi

      http.isOk >/dev/null
    else
      echo "DRYRUN: >>>>> $_paramAction $_paramObj [${_object_name}] >> $_sMethod $_sUrl $_jsondata"
      echo "... _bShowResponse: $_bShowResponse"
      echo "... _bSendData    : $_bSendData"
    fi

    # --- on list action: filter response
    if [ "${_paramAction}" = "list" ]; then
      if [ ${_paramDryrun} = false ]; then
        _wd ">>>>> filter response by [object_name]"
        if [ $_bShowFilter = true ]; then
          http.getResponse | grep object_name | cut -f 2- -d ":" | sed 's#^ "##g' | sed 's#",$##'
        else
          http.getResponse | grep object_name | cut -f 2- -d ":" | sed 's#^ "##g' | sed 's#",$##' >/dev/null
        fi

      else

        echo "DRYRUN: >>>>> filter response by [object_name]"
        echo
      fi
    fi

    # --- on exist action: filter response
    if [ "${_paramAction}" = "exists" -a ! -z "${_existFilter}" ]; then
      if [ ${_paramDryrun} = false ]; then
        _wd ">>>>> filter response by [$_existFilter]"
        if [ $_bShowFilter = true ]; then
          http.getResponse | grep $_existFilter
        else
          http.getResponse | grep $_existFilter >/dev/null
        fi
      else

        echo "DRYRUN: >>>>> filter response by [$_existFilter]"
        echo
      fi
    fi
  }

# ----------------------------------------------------------------------
# functions for Host
# ----------------------------------------------------------------------



  # ............................................................
  # helper to create a base config for the current host
  # UNUSED
  function UNUSED_initHostdata(){
    export CFGSTORAGE="directorhost"
    (
      $ch --flush
      $ch --set  object_name   \"$MY_NAME\"
      $ch --set  object_type   '"object"'
      $ch --set  address       \"$MY_IP\"
      $ch --set  display_name  \"$MY_NAME\"
    ) 2>/dev/null
  }


  # ............................................................
  # create a host with PUT on director API
  function hostCreate(){
    _h2 "create host"

    ObjAction create host
    if [ -z "`http.isOk`" ]; then
      echo "ERROR, host was NOT created."
    else
      echo "OK, host was created successfully."
    fi
  }


  # ............................................................
  # get data of current host from director API
  # and by the way it updates the local host infos too.
  function hostRead(){
    _h2 "read host"
    ObjAction read host
    if [ -z "`http.isOk`" ]; then
      echo "ERROR, host was NOT found."
    fi
  }


  # ............................................................
  # update current host
  # param  JSON part starting with ", " and some json data
  function hostUpdate(){
    _h2 "update host - set $1 $2"

    ObjAction update host
    if [ -z "`http.isOk`" ]; then
      case `http.getStatuscode` in
        "304")
          echo "OK, no update"
          ;;
        *)
          echo "ERROR during update of the host data"
      esac
    else
      echo "OK, host was updated"
    fi
  }
  # ............................................................
  # ensure that a host exists
  function hostCreateOrUpdate(){
    ObjAction exists host
    if [ $? -ne 0 ]; then
      hostCreate
    else
      hostUpdate
    fi
  }



  # ............................................................
  # delete the current host in the director
  function hostDelete(){
    _h2 "delete host"
    ObjAction delete host
    case `http.getStatuscode` in
      "200")
        echo "OK, host was deleted"
        ;;
      "404")
        echo "ERROR, host does not exist"
        ;;
      *)
        echo "ERROR, unable to delete host"
    esac
    flushDatadir
  }



# ----------------------------------------------------------------------
# functions for services
# director/service
# ----------------------------------------------------------------------

  # ............................................................
  # helper for services: generate variable names for a check
  # vars have prefix IDC for "Icinga Director"
  # uses global var checkName
  function _generateVarsByCheckname(){
    if [ -z "${checkName}" ]; then
      echo ERROR: checkName is empty - I guess _parseCheckConfig was not executed.
      exit 1
    fi

    IDC_command__obj_name="${checkName}"
    IDC_service__obj_name="service-template_for_command_${checkName}"
    IDC_svcathost__obj_name="`_getName4Svcathost ${checkName}`"
    # IDC_service__obj_name="${checkName}"
    # IDC_svcathost__obj_name="${checkName}"

    IDC_command__cachefile="${dir_data}/command_${checkName}__at-director.txt"
    # IDC_service__cachefile="${dir_data}/all_defined_services__at-director.txt"
    # IDC_svcathost__cachefile="${dir_data}/all_services_on_host__at-director.txt"
  }



# ............................................................
  # create a service in Icinga director
  # uses global variables only
  function serviceCreateOrUpdate(){
    _h2 "${FUNCNAME[0]}()"

    local sMode=update
    local sMethod=POST
    local _sUrl="director/service?name=${IDC_service__obj_name}"

    ObjAction exists service
    if [ $? -ne 0 ]; then
      ObjAction create service
    else
      ObjAction update service
    fi
    if [ $? -ne 0 ]; then
      echo "ERROR :/"
    else
      echo "OK"
    fi
  }


  # ............................................................
  # create a service in Icinga director
  # param   string  filename of check config
  function serviceCreateByCfgFile(){
      _h2 "${FUNCNAME[0]}($1) - create single service of given file"
      _parseCheckConfig "${1}"
      _generateVarsByCheckname "${checkName}"

      # create command if it does not exist
      serviceCreateOrUpdate

      ObjAction exists service
      if [ $? -eq 0 ]; then
        ObjAction exists svclink
        if [ $? -ne 0 ]; then
          ObjAction create svclink
        else
          echo "SKIP: linked service on host [${IDC_svcathost__obj_name}] exists"
          # TODO, uncomment -- wenn es sinnvolle Features gibt
          # ObjAction update svclink
        fi
      else
        echo "SKIP link ... service template for ${checkName} not ready."
      fi
  }

  # ............................................................
  # create all functions; this functon is called with cli parameter
  # no params
  function servicesCreateAll(){
    _h2 "${FUNCNAME[0]}() - create all services"

    # loop over all services and create
    for mycheckfile in `getChecks`
    do
      serviceCreateByCfgFile "${mycheckfile}"
      echo
    done
  }


  # ............................................................
  # cleanup services - delete unneded links
  function svclinkCleanup(){
    _h2 "Cleanup linked service templates"
    tmpRemote=/tmp/remoteLinks
    tmpLocal=/tmp/localChecks


    # --- perpare I: create file with remote service template links
    ObjAction list svclink >$tmpRemote

    # --- perpare II: create file with local configs and object names for its link
    rm -f $tmpLocal 2>/dev/null
    for mycheckfile in `getChecks`
    do
      _parseCheckConfig "${mycheckfile}"
      _generateVarsByCheckname "${checkName}"
      echo "$mycheckfile:${IDC_svcathost__obj_name}" >>$tmpLocal
    done

    # _h3 "local checks"
    # cat $tmpLocal
    # _h3 "remote linked service templates"
    # ObjAction list svclink

    # --- Compare ...
    # _h3 "Compare"
    cat $tmpRemote | while read remoteLink
    do
      grep $remoteLink $tmpLocal >/dev/null
      if [ $? -eq 0 ]; then
        echo "OK: $remoteLink"
      else
        echo "DELETE: link [$remoteLink] is not used by any local check anymore."
        checkName=`_getName4Svcathost $remoteLink reverse`
        _generateVarsByCheckname "${checkName}"
        ObjAction delete svclink
      fi
    done
    rm -f $tmpLocal $tmpRemote
    echo --- done.
  }


# ----------------------------------------------------------------------
# functions for Director
# ----------------------------------------------------------------------

  # ............................................................
  # kick the director to deploy config changes now
  function directorDeploy(){
    _h2 deploy
    _initHttp

    _wd POST director/config/deploy
    _APIcall POST director/config/deploy
    if [ -z "`http.isOk`" ]; then
      echo "ERROR deploy config was not queued."
    else
      echo "OK, deploy was triggered"
    fi
  }

# ..................................................................
#
# show a help
function showHelp(){
script=`basename $0`
cat <<ENDOFHELP

HELP:

Host actions

  --hc
  --hostcreate
    Create the host [$MY_NAME] in the icinga director

  --hr
  --hostread
    Read the host information of [$MY_NAME] in the icinga director

  --hu
  --hostupdate
    Update host in the Icinga director

  --he
  --hostensure
    Create host if it does not exist otherwise update it in the
    Icinga director

  --hd
  --hostdelete
    Delete [$MY_NAME] in the icinga director


Check actions

  --listchecks
    list all local config files of known checks for [$MY_NAME]

  --linkcleanup
    verify added service templates on [$MY_NAME] with locally
    defined checks and remove unneeded items.

  --sca
  --servicescreateall
    Loop over defined checks and add all checks to the host.
    See also:
      --listchecks to get a list of config files.

  --sc CFGFILE
  --servicecreate CFGFILE
    create service by naming a config file
    See also:
      --listchecks to get a list of config files.

Cache

    --flushcache
      remove files in [$dir_data]

Director actions

  --deploy
    icinga update (deploy director data)

Other parameters

  --cfg CONFIGFILE
    load a costom config file; default: ./inc_getconfig.sh
    This must be the 1st parameter to be processed.

  --debug
    enable debug output.

  --nodebug
    disable debug output.

  --dryrun
    enable dryrun - it shows actions without execution.


EXAMPLES

  # $script --dryrun --he
  Show actions and api calls for "--he" parameter (create or update host)

ENDOFHELP
}

# ------------------------------------------------------------
#
# MAIN
#
# ------------------------------------------------------------

echo
echo "##### DIRECTOR HELPER $MY_NAME - $MY_IP"
echo

if [ "$1" = "--cfg" ] && [ -n "$2" ]; then
  echo "INFO: loading custom config [$2]..."
  . "${2}"
  shift 2
else
  . "$( dirname $0 )/inc_getconfig.sh"
fi
. `dirname $0`/inc_functions.sh
. `dirname $0`/inc/rest-api-client.sh


if [ $# -eq 0 ]; then
  showHelp
  exit 0
fi

cd `dirname $0`
ls ./`basename $0` >/dev/null || exit 1

_initVars

# ensure that ./inc_getconfig.sh was loaded
if [ -z "${dir_cfg}" ]; then
  echo ERROR: Client is not installed/ configured yet on this machine.
  exit 1
fi

while [ $# -gt 0 ];
do
  case "$1" in

    '--help' | '-h' | '-?')
      showHelp
      exit 0
      ;;

    '--debug')
      cfg_debug=true
      ;;

    '--nodebug')
      cfg_debug=false
      ;;

    '--dryrun')
      cfg_dryrun=true
      ;;
    # ----- override local data with those from a file
    '--load')
      if [ ! -f "$2" ]; then
        echo ERROR: file "$2" is not readable.
        echo Hint: ist must be an absolute path or relative to $( pwd )
        exit 1
      fi
      loadfile="$2"
      . "${loadfile}"
      shift
      echo "loaded ${loadfile}"
      ;;

    # ----- host actions

    '--he' | '--hostensure')
      hostCreateOrUpdate
      ;;
    '--hc' | '--hostcreate')
      hostCreate
      ;;
    '--hr' | '--hostread')
      hostRead
      ;;
    '--hu' | '--hostupdate')
      hostUpdate
      ;;
    '--hd' | '--hostdelete')
      hostDelete
      ;;
    '--hs' | '--hostshow')
      _generateJsonForHost
      $ch --json 2>/dev/null
      ;;

    # ----- check actions

    '--listchecks')
      getChecks
      ;;

    '--sca' | '--servicescreateall')
      servicesCreateAll
      ;;

    '--sc' | '--servicecreate')
      serviceCreateByCfgFile "${2}"
      shift
      ;;

    '--linkcleanup')
      svclinkCleanup
      shift
      ;;

    '--flushcache')
      flushDatadir
      ;;

    # ----- director

    '--deploy')
      directorDeploy
      exit 0
      ;;

    '--testrun')
      _h1 "svclink - without a config"
      ObjAction "create" "svclink" dry
      ObjAction "exists" "svclink" dry
      ObjAction "read"   "svclink" dry
      ObjAction "update" "svclink" dry
      ObjAction "delete" "svclink" dry

      _h1 "svclink - with reading a config"
      _parseCheckConfig "/etc/icinga2-passive-client/checks/CPU-usage"
      _generateVarsByCheckname "${checkName}"

      ObjAction "create" "svclink" dry
      ObjAction "exists" "svclink" dry
      ObjAction "read"   "svclink" dry
      ObjAction "update" "svclink" dry
      ObjAction "delete" "svclink" dry

      _h1 "host"
      ObjAction "create" "host" dry
      ObjAction "exists" "host" dry
      ObjAction "read"   "host" dry
      ObjAction "update" "host" dry
      ObjAction "delete" "host" dry

      ;;

    # ---- ab hier TODO
    u)
      shift 1
      hostUpdate "$*"
      exit 0
      ;;
    *)
      echo "ERROR: unknown parameter detected. No idea what to do with [$1]."
      echo "Exiting..."
      exit 2

  esac
  shift 1
done

exit 0
# ======================================================================