diff --git a/docker/.env b/docker/.env index 95e9205208b0087ccac0d3df8dea85ea610831c2..0ddeb5f9c941d1bfb86d50fb49c6d2ab21ea48fc 100644 --- a/docker/.env +++ b/docker/.env @@ -1,6 +1,6 @@ # ====================================================================== # -# GENERATED BY init.sh - template: ./templates/dot_env - e2cde05722688ff85d3a93e9cd55787e +# GENERATED BY init.sh - template: templates/dot_env - e2cde05722688ff85d3a93e9cd55787e # values to be used in docker-composer.yml # # ====================================================================== diff --git a/docker/containers/web-server/Dockerfile b/docker/containers/web-server/Dockerfile index 0c170625f65e6eb10181b31ff7ac139e951cbb8f..93975a1445afbe0b12dcc13b3973d6f9d8813769 100644 --- a/docker/containers/web-server/Dockerfile +++ b/docker/containers/web-server/Dockerfile @@ -1,13 +1,13 @@ # -# GENERATED BY init.sh - template: ./templates/web-server-Dockerfile - 42dce773c83597a7d05af398bdd66d15 +# GENERATED BY init.sh - template: templates/web-server-Dockerfile - 42dce773c83597a7d05af398bdd66d15 # -FROM php:8.2-apache +FROM php:8.4-apache # install packages RUN apt-get update && apt-get install -y git unzip zip libapache2-mod-xsendfile # enable apache modules -RUN a2enmod xsendfile +RUN a2enmod rewrite xsendfile # install php packages COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/ diff --git a/docker/containers/web-server/apache/sites-enabled/vhost_app.conf b/docker/containers/web-server/apache/sites-enabled/vhost_app.conf index 6d6e7fb93c99508aa2db13b821f858632966c90f..db985325251fd7540532b6720a2e3134e5ad382e 100644 --- a/docker/containers/web-server/apache/sites-enabled/vhost_app.conf +++ b/docker/containers/web-server/apache/sites-enabled/vhost_app.conf @@ -1,5 +1,5 @@ # -# GENERATED BY init.sh - template: ./templates/vhost_app.conf - 4dfd63417ad808a5ed00ffaf117464a8 +# GENERATED BY init.sh - template: templates/vhost_app.conf - 4dfd63417ad808a5ed00ffaf117464a8 # <VirtualHost *:80> DocumentRoot /var/www/ci-pkg/public_html diff --git a/docker/containers/web-server/php/extra-php-config.ini b/docker/containers/web-server/php/extra-php-config.ini index aa13bd779afa40bbfa25f10adef9baeae6d14f7d..8fc56969fb24b14a03349e4d16746d7607f67c2e 100644 --- a/docker/containers/web-server/php/extra-php-config.ini +++ b/docker/containers/web-server/php/extra-php-config.ini @@ -1,5 +1,5 @@ ; -; GENERATED BY init.sh - template: ./templates/extra-php-config.ini - 9dce36d285d5b21d70e015c074c196c2 +; GENERATED BY init.sh - template: templates/extra-php-config.ini - 9dce36d285d5b21d70e015c074c196c2 ; [PHP] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 39e932e040f09a3d0ac65254cad4b6cf6c275885..85be423d9b110f533cdac0c2b76335f1caa01e22 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,5 +1,5 @@ # -# GENERATED BY init.sh - template: ./templates/docker-compose.yml - fc2f1d55926abdb9c54f65afd0571d7b +# GENERATED BY init.sh - template: templates/docker-compose.yml - fc2f1d55926abdb9c54f65afd0571d7b # # ====================================================================== # @@ -19,7 +19,7 @@ services: build: context: . dockerfile: ./containers/web-server/Dockerfile - image: "php:8.2-apache" + image: "php:8.4-apache" container_name: 'ci-pkg-server' ports: - '${APP_PORT}:80' diff --git a/docker/init.sh b/docker/init.sh index 060db24942ad10e1bb0f3abca96443d85f55d289..04fc725623d657387c79c9ad4644cffe7c5bbc95 100755 --- a/docker/init.sh +++ b/docker/init.sh @@ -4,53 +4,354 @@ # DOCKER PHP DEV ENVIRONMENT :: INIT # # ---------------------------------------------------------------------- -# 2021-11-nn v1.0 <axel.hahn@iml.unibe.ch> -# 2022-07-19 v1.1 <axel.hahn@iml.unibe.ch> support multiple dirs for setfacl -# 2022-11-16 v1.2 <www.axel-hahn.de> use docker-compose -p "$APP_NAME" -# 2022-12-18 v1.3 <www.axel-hahn.de> add -p "$APP_NAME" in other docker commands -# 2022-12-20 v1.4 <axel.hahn@unibe.ch> replace fgrep with grep -F -# 2023-03-06 v1.5 <www.axel-hahn.de> up with and without --build -# 2023-08-17 v1.6 <www.axel-hahn.de> menu selection with single key (without return) +# 2021-11-nn v1.0 <axel.hahn@iml.unibe.ch> +# 2022-07-19 v1.1 <axel.hahn@iml.unibe.ch> support multiple dirs for setfacl +# 2022-11-16 v1.2 <www.axel-hahn.de> use docker-compose -p "$APP_NAME" +# 2022-12-18 v1.3 <www.axel-hahn.de> add -p "$APP_NAME" in other docker commands +# 2022-12-20 v1.4 <axel.hahn@unibe.ch> replace fgrep with grep -F +# 2023-03-06 v1.5 <www.axel-hahn.de> up with and without --build +# 2023-08-17 v1.6 <www.axel-hahn.de> menu selection with single key (without return) +# 2023-11-10 v1.7 <axel.hahn@unibe.ch> replace docker-compose with "docker compose" +# 2023-11-13 v1.8 <axel.hahn@unibe.ch> UNDO "docker compose"; update infos +# 2023-11-15 v1.9 <axel.hahn@unibe.ch> add help; execute multiple actions by params; new menu item: open app +# 2023-12-07 v1.10 <www.axel-hahn.de> simplyfy console command; add php linter +# 2024-07-01 v1.11 <www.axel-hahn.de> diff with colored output; suppress errors on port check +# 2024-07-19 v1.12 <axel.hahn@unibe.ch> apply shell fixes +# 2024-07-22 v1.13 <axel.hahn@unibe.ch> show info if there is no database container; speedup replacements +# 2024-07-22 v1.14 <axel.hahn@unibe.ch> show colored boxes with container status +# 2024-07-24 v1.15 <axel.hahn@unibe.ch> update menu output +# 2024-07-26 v1.16 <axel.hahn@unibe.ch> hide unnecessary menu items (WIP) +# 2024-07-29 v1.17 <www.axel-hahn.de> hide unnecessary menu items; reorder functions +# 2024-08-14 v1.18 <www.axel-hahn.de> update container view +# 2024-09-20 v1.19 <www.axel-hahn.de> detect dockerd-rootless (hides menu item to set permissions) +# 2024-10-16 v1.20 <axel.hahn@unibe.ch> add db import and export +# 2024-10-25 v1.21 <axel.hahn@unibe.ch> create missing subdir dbdumps +# 2024-10-30 v1.22 <axel.hahn@unibe.ch> added: Open Mysql client in container +# 2024-10-30 v1.23 <axel.hahn@unibe.ch> added: show menu hints why some menu items are visible +# 2024-11-20 v1.24 <axel.hahn@unibe.ch> fix menu with started database less app; apply template permissions on target file; add $WEBURL; remove $frontendurl +# 2024-11-20 v1.25 <axel.hahn@unibe.ch> fix menu startup containers +# 2024-11-21 v1.26 <axel.hahn@unibe.ch> Reset colors in _checkConfig # ====================================================================== -cd $( dirname $0 ) -. $( basename $0 ).cfg +cd "$( dirname "$0" )" || exit 1 + +_version="1.26" + +# init used vars +gittarget= +WEBURL= + +_self=$( basename "$0" ) + +# shellcheck source=/dev/null +. "${_self}.cfg" || exit 1 + # git@git-repo.iml.unibe.ch:iml-open-source/docker-php-starterkit.git selfgitrepo="docker-php-starterkit.git" -_version="1.6" +fgGray="\e[1;30m" +fgRed="\e[31m" +fgGreen="\e[32m" +fgBrown="\e[33m" +fgBlue="\e[34m" + +fgInvert="\e[7m" +fgReset="\e[0m" + +# ----- status varsiables +# running containers +DC_WEB_UP=0 +DC_DB_UP=0 +DC_ALL_UP=0 + +# repo of docker-php-starterkit is here? +DC_REPO=1 + +DC_CONFIG_CHANGED=0 + +# absolute urls for web app +DC_WEB_URL="" + +DC_DUMP_DIR=dbdumps +DC_SHOW_MENUHINTS=0 + +isDockerRootless=0 +ps -ef | grep dockerd-rootless | grep -q $USER && isDockerRootless=1 # ---------------------------------------------------------------------- # FUNCTIONS # ---------------------------------------------------------------------- +# check config for changes in newer versions +function _checkConfig(){ + + # --- v1.24 + if [ -z "$WEBURL" ]; then + echo -e "${fgBrown}INFO: add 'WEBURL=\"/\"' in your ${_self}.cfg. It is a new var since v1.24${fgReset}" + WEBURL="/" + fi + if [ -n "$frontendurl" ]; then + echo -e "${fgBrown}INFO: Remove frontendurl=$frontendurl in your ${_self}.cfg. It is obsolete since v1.24${fgReset}" + fi + +} +# ---------------------------------------------------------------------- +# STATUS FUNCTIONS + +# get container status and set global variable DC_REPO +# DC_REPO = 0 nothing to do - repo was changed to project +# DC_REPO = 1 if repo is in selfgitrepo (must be deleted) +function _getStatus_repo(){ + DC_REPO=0 + git config --get remote.origin.url 2>/dev/null | grep -q $selfgitrepo && DC_REPO=1 +} + +# check if any of the templates has a change that must be applied +function _getStatus_template(){ + _generateFiles "dryrun" +} + +# get container status and set global variables +# DC_WEB_UP - web container +# DC_DB_UP - database container +# 0 = down +# 1 = up +function _getStatus_docker(){ + local _out + _out=$( docker-compose -p "$APP_NAME" ps) + + DC_WEB_UP=0 + DC_DB_UP=0 + DC_ALL_UP=0 + + grep -q "${APP_NAME}-server" <<< "$_out" && DC_WEB_UP=1 + grep -q "${APP_NAME}-db" <<< "$_out" && DC_DB_UP=1 + + if [ "$DB_ADD" != "false" ] && [ ! -d "${DC_DUMP_DIR}" ]; then + echo "INFO: creating subdir ${DC_DUMP_DIR} to import/ export databases ..." + mkdir "${DC_DUMP_DIR}" || exit 1 + return + fi + + if [ "${DC_WEB_UP}" = "1" ] && [ "${DC_DB_UP}" = "1" ]; then + DC_ALL_UP=1 + fi + + if [ "$DB_ADD" = "false" ] && [ "${DC_WEB_UP}" = "1" ]; then + DC_ALL_UP=1 + fi + +} + +# Get web url of the application +# It is for support of Nginx Docker Proxy +# https://github.com/axelhahn/nginx-docker-proxy +# It returns http://localhost:<port> or a https://<appname> plus $WEBURL +function _getWebUrl(){ + if grep -q "^[0-9\.]* ${APP_NAME}-server" /etc/hosts; then + DC_WEB_URL="https://${APP_NAME}-server$WEBURL" + else + DC_WEB_URL=http://localhost:${APP_PORT}$WEBURL + fi + set +vx +} + +# ---------------------------------------------------------------------- +# OUTPUT + # draw a headline 2 function h2(){ echo - echo -e "\e[33m>>>>> $*\e[0m" + echo -e "$fgBrown>>>>> $*$fgReset" } # draw a headline 3 function h3(){ echo - echo -e "\e[34m----- $*\e[0m" + echo -e "$fgBlue----- $*$fgReset" +} + +# helper for menu: print an inverted key +function _key(){ + echo -en "$fgInvert ${1} $fgReset" +} + +# helper for menu: show hint text +# param int FLag _bAll (i true the txt will be hidden) +# param string message to show +function menuhint(){ + local _bAll="$1" + shift 1 + test $DC_SHOW_MENUHINTS -ne 0 && test "$_bAll" -eq "0" && ( echo -e "$fgBlue $*$fgReset" ) +} + +# show menu in interactive mode and list keys in help with param -h +# param string optional: set to "all" to show all menu items +function showMenu(){ + + local _bAll=0 + test -n "$1" && _bAll=1 + + local _spacer=" " + + echo + if [ $DC_REPO -eq 1 ] || [ $_bAll -eq 1 ]; then + menuhint $_bAll "Git data of starterkit were found" + echo "${_spacer}$( _key g ) - remove git data of starterkit" + echo + fi + + if [ $isDockerRootless -eq 1 ] || [ $_bAll -eq 1 ]; then + menuhint $_bAll "Because rootless docker was found" + echo "${_spacer}$( _key i ) - init application: set permissions" + echo + fi + + if [ $DC_CONFIG_CHANGED -eq 1 ] || [ $_bAll -eq 1 ]; then + menuhint $_bAll "Config was changed" + echo "${_spacer}$( _key t ) - generate files from templates" + echo + fi + if [ $DC_CONFIG_CHANGED -eq 0 ] || [ $_bAll -eq 1 ]; then + menuhint $_bAll "Config is unchanged" + echo "${_spacer}$( _key T ) - remove generated files" + echo + fi + if [ $DC_ALL_UP -eq 0 ] || [ $_bAll -eq 1 \ + ]; then + if [ $DC_CONFIG_CHANGED -eq 0 ] || [ $_bAll -eq 1 ]; then + menuhint $_bAll "A container is down and config is unchanged" + echo "${_spacer}$( _key u ) - startup containers docker-compose ... up -d" + echo "${_spacer}$( _key U ) - startup containers docker-compose ... up -d --build" + echo + echo "${_spacer}$( _key r ) - remove containers docker-compose rm -f" + echo + fi + fi + if [ $DC_WEB_UP -eq 1 ] || [ $DC_DB_UP -eq 1 ] || [ $_bAll -eq 1 ]; then + menuhint $_bAll "A container is up" + echo "${_spacer}$( _key s ) - shutdown containers docker-compose stop" + echo + echo "${_spacer}$( _key m ) - more infos" + echo "${_spacer}$( _key o ) - open app [${APP_NAME}] $DC_WEB_URL" + echo "${_spacer}$( _key c ) - console (bash)" + echo + fi + if [ $DC_WEB_UP -eq 1 ] || [ $_bAll -eq 1 ]; then + menuhint $_bAll "Web container is up" + echo "${_spacer}$( _key p ) - console check with php linter" + echo + fi + if [ $DC_DB_UP -eq 1 ] || [ $_bAll -eq 1 ]; then + echo + menuhint $_bAll "Database container is up" + echo "${_spacer}$( _key d ) - Dump container database" + echo "${_spacer}$( _key D ) - Import Dump into container database" + echo "${_spacer}$( _key M ) - Open Mysql client in database container" + echo + fi + menuhint $_bAll "Always available" + echo "${_spacer}$( _key q ) - quit" + +} +function showHelp(){ + cat <<EOH + +INITIALIZER FOR DOCKER APP v$_version + +A helper script written in Bash to bring up a PHP+Mysql application in docker. + +📄 Source : https://git-repo.iml.unibe.ch/iml-open-source/docker-php-starterkit +📗 Docs : https://os-docs.iml.unibe.ch/docker-php-starterkit/ +📜 License: GNU GPL 3.0 +(c) Institute for Medical Education; University of Bern + + +SYNTAX: + $_self [-h|-v] + $_self [menu key [.. menu key N]] + +OPTIONS: + -h show this help and exit + -v show version exit + +MENU KEYS: + In the interactive menu are some keys to init an action. + The same keys can be put as parameter to start this action. + You can add multiples keys to apply multiple actions. + +$( showMenu "all" ) + +EXAMPLES: + + $_self starts interactive mode + $_self u bring up docker container(s) and stay in interactive mode + $_self i q set write permissions and quit + $_self p q start php linter and exit + +EOH +} + + +# show urls for app container +function _showBrowserurl(){ + echo "In a web browser open:" + echo " $DC_WEB_URL" +} + +# detect + show ports and urls for app container and db container +function _showInfos(){ + _showContainers long + h2 INFO + + h3 "processes webserver" + # docker-compose top + docker top "${APP_NAME}-server" + if [ ! "$DB_ADD" = "false" ]; then + h3 "processes database" + docker top "${APP_NAME}-db" + fi + + h3 "What to open in browser" + if echo >"/dev/tcp/localhost/${APP_PORT}"; then + # echo "OK, app port ${APP_PORT} is reachable" + # echo + _showBrowserurl + else + echo "ERROR: app port ${APP_PORT} is not available" + fi 2>/dev/null + + if [ "$DB_ADD" != "false" ]; then + h3 "Check database port" + if echo >"/dev/tcp/localhost/${DB_PORT}"; then + echo "OK, db port ${DB_PORT} is reachable" + echo + echo "In a local DB admin tool you can connect it:" + echo " host : localhost" + echo " port : ${DB_PORT}" + echo " user : root" + echo " password: ${MYSQL_ROOT_PASS}" + else + echo "NO, db port ${DB_PORT} is not available" + fi 2>/dev/null + + fi + echo } -# function _gitinstall(){ -# h2 "install/ update app from git repo ${gitrepo} in ${gittarget} ..." -# test -d ${gittarget} && ( cd ${gittarget} && git pull ) -# test -d ${gittarget} || git clone -b ${gitbranch} ${gitrepo} ${gittarget} -# } +# ---------------------------------------------------------------------- +# ACTIONS # set acl on local directory function _setWritepermissions(){ h2 "set write permissions on ${gittarget} ..." - local _user=$( id -gn ) - typeset -i local _user_uid=0 - test -f /etc/subuid && _user_uid=$( grep $_user /etc/subuid 2>/dev/null | cut -f 2 -d ':' )-1 - typeset -i local DOCKER_USER_OUTSIDE=$_user_uid+$DOCKER_USER_UID + local _user; _user=$( id -gn ) + local _user_uid; typeset -i _user_uid=0 + + test -f /etc/subuid && _user_uid=$( grep "$_user" /etc/subuid 2>/dev/null | cut -f 2 -d ':' )-1 + local DOCKER_USER_OUTSIDE; typeset -i DOCKER_USER_OUTSIDE=$_user_uid+$DOCKER_USER_UID set -vx @@ -62,10 +363,10 @@ function _setWritepermissions(){ sudo setfacl -bR "${mywritedir}" # default permissions: both the host user and the user with UID 33 (www-data on many systems) are owners with rwx perms - sudo setfacl -dRm u:${DOCKER_USER_OUTSIDE}:rwx,${_user}:rwx "${mywritedir}" + sudo setfacl -dRm "u:${DOCKER_USER_OUTSIDE}:rwx,${_user}:rwx" "${mywritedir}" # permissions: make both the host user and the user with UID 33 owner with rwx perms for all existing files/directories - sudo setfacl -Rm u:${DOCKER_USER_OUTSIDE}:rwx,${_user}:rwx "${mywritedir}" + sudo setfacl -Rm "u:${DOCKER_USER_OUTSIDE}:rwx,${_user}:rwx" "${mywritedir}" done set +vx @@ -76,11 +377,10 @@ function _removeGitdata(){ h2 "Remove git data of starterkit" echo -n "Current git remote url: " git config --get remote.origin.url - git config --get remote.origin.url 2>/dev/null | grep $selfgitrepo >/dev/null - if [ $? -eq 0 ]; then + if git config --get remote.origin.url 2>/dev/null | grep -q $selfgitrepo; then echo echo -n "Delete local .git and .gitignore? [y/N] > " - read answer + read -r answer test "$answer" = "y" && ( echo "Deleting ... " && rm -rf ../.git ../.gitignore ) else echo "It was done already - $selfgitrepo was not found." @@ -92,36 +392,64 @@ function _removeGitdata(){ # see _generateFiles() function _fix_no-db(){ local _file=$1 - if [ $DB_ADD = false ]; then - typeset -i local iStart=$( cat ${_file} | grep -Fn "$CUTTER_NO_DATABASE" | cut -f 1 -d ':' )-1 + if [ "$DB_ADD" = "false" ]; then + local iStart; typeset -i iStart + iStart=$( grep -Fn "$CUTTER_NO_DATABASE" "${_file}" | cut -f 1 -d ':' )-1 if [ $iStart -gt 0 ]; then - sed -ni "1,${iStart}p" ${_file} + sed -n "$sed_no_backup" "1,${iStart}p" "${_file}" fi fi } +# helper function to generate replacements using sed +# it loops over all vars in the config file +# used in _generateFiles +function _getreplaces(){ + # loop over vars to make the replacement + grep "^[a-zA-Z]" "$_self.cfg" | while read -r line + do + # echo replacement: $line + mykey=$( echo "$line" | cut -f 1 -d '=' ) + myvalue="$( eval echo \"\$"$mykey"\" )" + + # TODO: multiline values fail here in replacement with sed + echo -e "s#{{$mykey}}#${myvalue}#g" + + done +} + # loop over all files in templates subdir make replacements and generate # a target file. # It skips if # - 1st line is not starting with "# TARGET: filename" # - target file has no updated lines +# If the 1st parameter is set to "dryrun" it will not generate files. +# param string dryrun optional: set to "dryrun" to not generate files function _generateFiles(){ - # re-read config vars - . $( basename $0 ).cfg + local _dryrun="$1" + DC_CONFIG_CHANGED=0 + + # shellcheck source=/dev/null + . "${_self}.cfg" || exit 1 + + params=$( _getreplaces | while read -r line; do echo -n "-e '$line' "; done ) local _tmpfile=/tmp/newfilecontent$$.tmp - h2 "generate files from templates..." - for mytpl in $( ls -1 ./templates/* ) + + test "$_dryrun" = "dryrun" || h2 "generate files from templates..." + for mytpl in templates/* do # h3 $mytpl local _doReplace=1 # fetch traget file from first line - target=$( head -1 $mytpl | grep "^# TARGET:" | cut -f 2- -d ":" | awk '{ print $1 }' ) + target=$( head -1 "$mytpl" | grep "^# TARGET:" | cut -f 2- -d ":" | awk '{ print $1 }' ) if [ -z "$target" ]; then - echo SKIP: $mytpl - target was not found in 1st line + if [ "$_dryrun" != "dryrun" ]; then + echo "SKIP: $mytpl - target was not found in 1st line" + fi _doReplace=0 fi @@ -129,39 +457,38 @@ function _generateFiles(){ if [ $_doReplace -eq 1 ]; then # write file from line 2 to a tmp file - sed -n '2,$p' $mytpl >$_tmpfile + sed -n '2,$p' "$mytpl" >"$_tmpfile" + chmod "$( stat -c %a "$mytpl" )" "$_tmpfile" # add generator # sed -i "s#{{generator}}#generated by $0 - template: $mytpl - $( date )#g" $_tmpfile - local _md5=$( md5sum $_tmpfile | awk '{ print $1 }' ) - sed -i "s#{{generator}}#GENERATED BY $( basename $0 ) - template: $mytpl - $_md5#g" $_tmpfile - - # loop over vars to make the replacement - grep "^[a-zA-Z]" $( basename $0 ).cfg | while read line - do - # echo replacement: $line - mykey=$( echo $line | cut -f 1 -d '=' ) - myvalue="$( eval echo \"\${$mykey}\" )" - # grep "{{$mykey}}" $_tmpfile - - # TODO: multiline values fail here in replacement with sed - sed -i "s#{{$mykey}}#${myvalue}#g" $_tmpfile - done + local _md5; _md5=$( md5sum $_tmpfile | awk '{ print $1 }' ) + sed -i "$sed_no_backup" "s#{{generator}}#GENERATED BY $_self - template: $mytpl - $_md5#g" $_tmpfile + + # apply all replacements to the tmp file + eval sed "$sed_no_backup" "$params" "$_tmpfile" || exit + _fix_no-db $_tmpfile # echo "changes for $target:" - diff "../$target" "$_tmpfile" | grep -v "$_md5" | grep -v "^---" | grep . - if [ $? -eq 0 -o ! -f "../$target" ]; then - echo -n "$mytpl - changes detected - writing [$target] ... " - mkdir -p $( dirname "../$target" ) || exit 2 - mv "$_tmpfile" "../$target" || exit 2 - echo OK + if diff --color=always "../$target" "$_tmpfile" 2>/dev/null | grep -v "$_md5" | grep -v "^---" | grep . || [ ! -f "../$target" ]; then + if [ "$_dryrun" = "dryrun" ] + then + DC_CONFIG_CHANGED=1 + else + echo -n "$mytpl - changes detected - writing [$target] ... " + mkdir -p "$( dirname ../"$target" )" || exit 2 + mv "$_tmpfile" "../$target" || exit 2 + echo -e "${fgGreen}OK${fgReset}" + echo + fi else rm -f $_tmpfile - echo "SKIP: $mytpl - Nothing to do." + if [ "$_dryrun" != "dryrun" ]; then + echo "SKIP: $mytpl - Nothing to do." + fi fi fi - echo done } @@ -170,104 +497,200 @@ function _generateFiles(){ # a traget file. function _removeGeneratedFiles(){ h2 "remove generated files..." - for mytpl in $( ls -1 ./templates/* ) + for mytpl in templates/* do - h3 $mytpl + h3 "$mytpl" # fetch traget file from first line - target=$( head -1 $mytpl | grep "^# TARGET:" | cut -f 2- -d ":" | awk '{ print $1 }' ) + target=$( head -1 "$mytpl" | grep "^# TARGET:" | cut -f 2- -d ":" | awk '{ print $1 }' ) - if [ ! -z "$target" -a -f "../$target" ]; then + if [ -n "$target" ] && [ -f "../$target" ]; then echo -n "REMOVING " ls -l "../$target" || exit 2 rm -f "../$target" || exit 2 - echo OK + echo -e "${fgGreen}OK${fgReset}" else - echo SKIP: $target + echo "SKIP: $target" fi done } + +# show running containers function _showContainers(){ local bLong=$1 - h2 CONTAINERS - if [ -z "$bLong" ]; then - docker-compose -p "$APP_NAME" ps - else - docker ps | grep $APP_NAME + + local _out + + local sUp=".. UP" + local sDown=".. down" + + local Status= + local StatusWeb="$sDown" + local StatusDb="$sDown" + local colWeb= + local colDb= + + colDb="$fgRed" + colWeb="$fgRed" + + if [ $DC_WEB_UP -eq 1 ]; then + colWeb="$fgGreen" + StatusWeb="$sUp" + fi + + if [ $DC_DB_UP -eq 1 ]; then + colDb="$fgGreen" + StatusDb="$sUp" fi -} + if [ "$DB_ADD" = "false" ]; then + colDb="$fgGray" + local StatusDb=".. N/A" + Status="This app has no database container." + fi -# a bit stupid ... i think I need to delete it. -function _showInfos(){ - _showContainers long - h2 INFO + h2 CONTAINERS - h3 "processes" - docker-compose top + echo + printf " $colWeb$fgInvert %-32s $fgReset $colDb$fgInvert %-32s $fgReset\n" "WEB ${StatusWeb}" "DB ${StatusDb}" + printf " %-32s $fgReset %-32s $fgReset\n" "PHP ${APP_PHP_VERSION}" "${MYSQL_IMAGE}" + printf " %-32s $fgReset %-32s $fgReset\n" ":${APP_PORT}" ":${DB_PORT}" + + echo - h3 "Check app port" - >/dev/tcp/localhost/${APP_PORT} 2>/dev/null && ( - echo "OK, app port ${APP_PORT} is reachable" + if [ -n "$Status" ]; then + echo " $Status" echo - echo "In a web browser open:" - echo " $frontendurl" - ) - h3 "Check database port" - >/dev/tcp/localhost/${DB_PORT} 2>/dev/null && ( - echo "OK, db port ${DB_PORT} is reachable" + fi + + if [ -n "$bLong" ]; then + echo "$_out" + + h2 STATS + docker stats --no-stream echo - echo "In a local DB admin tool:" - echo " host : localhost" - echo " port : ${DB_PORT}" - echo " user : root" - echo " password: ${MYSQL_ROOT_PASS}" - ) - echo -} + fi -# helper for menu: print an inverted key -function _key(){ - printf "\e[4;7m ${1} \e[0m" } # helper: wait for a return key function _wait(){ - echo -n "... press RETURN > "; read -r + local _wait=15 + echo -n "... press RETURN ... or wait $_wait sec > "; read -r -t $_wait + echo +} + +# DB TOOL - dump db from container +function _dbDump(){ + local _iKeepDumps; + typeset -i _iKeepDumps=5 + local _iStart; + typeset -i _iStart=$_iKeepDumps+1; + + if [ $DC_DB_UP -eq 0 ]; then + echo "Database container is not running. Aborting." + return + fi + outfile=${DC_DUMP_DIR}/${MYSQL_DB}_$( date +%Y%m%d_%H%M%S ).sql + echo -n "dumping ${MYSQL_DB} ... " + if docker exec -i "${APP_NAME}-db" mysqldump -uroot -p${MYSQL_ROOT_PASS} ${MYSQL_DB} > "$outfile"; then + echo -n "OK ... Gzip ... " + if gzip "${outfile}"; then + echo "OK" + ls -l "$outfile.gz" + + # CLEANUP + echo + echo "--- Cleanup: keep $_iKeepDumps files." + ls -1t ${DC_DUMP_DIR}/* | sed -n "$_iStart,\$p" | while read -r delfile + do + echo "CLEANUP: Deleting $delfile ... " + rm -f "$delfile" + done + echo + echo -n "Size of dump directory: " + du -hs ${DC_DUMP_DIR} | awk '{ print $1 }' + + else + echo "ERROR" + rm -f "$outfile" + fi + else + echo "ERROR" + rm -f "$outfile" + fi +} + +# DB TOOL - import local database dump into container +function _dbImport(){ + echo "--- Available dumps:" + ls -ltr ${DC_DUMP_DIR}/*.gz | sed "s#^# #g" + if [ $DC_DB_UP -eq 0 ]; then + echo "Database container is not running. Aborting." + return + fi + echo -n "Dump file to import into ${MYSQL_DB} > " + read -r dumpfile + if [ -z "$dumpfile" ]; then + echo "Abort - no value was given." + return + fi + if [ ! -f "$dumpfile" ]; then + echo "Abort - wrong filename." + return + fi + + echo -n "Importing $dumpfile ... " + + # Mac OS compatibility + # if zcat "$dumpfile" | docker exec -i "${APP_NAME}-db" mysql -uroot -p${MYSQL_ROOT_PASS} "${MYSQL_DB}" + if cat "$dumpfile" | zcat | docker exec -i "${APP_NAME}-db" mysql -uroot -p${MYSQL_ROOT_PASS} "${MYSQL_DB}" + then + echo "OK" + else + echo "ERROR" + fi } # ---------------------------------------------------------------------- # MAIN # ---------------------------------------------------------------------- -action=$1 +_checkConfig + +# Mac OS compatibility +case "$OSTYPE" in + darwin*|bsd*) + sed_no_backup=" -i '' " + ;; + *) + sed_no_backup="-i" + ;; +esac + +action=$1; shift 1 while true; do - echo - echo -e "\e[32m===== INITIALIZER FOR DOCKER APP [$APP_NAME] v$_version ===== \e[0m\n\r" - if [ -z "$action" ]; then + _getStatus_repo + _getStatus_docker + _getStatus_template + _getWebUrl - _showContainers + if [ -z "$action" ]; then - h2 MENU - echo " $( _key g ) - remove git data of starterkit" - echo - echo " $( _key i ) - init application: set permissions" - echo " $( _key t ) - generate files from templates" - echo " $( _key T ) - remove generated files" + echo "_______________________________________________________________________________" echo - echo " $( _key u ) - startup containers docker-compose ... up -d" - echo " $( _key U ) - startup containers docker-compose ... up -d --build" - echo " $( _key s ) - shutdown containers docker-compose stop" - echo " $( _key r ) - remove containers docker-compose rm -f" + printf " %-70s ______\n" "${APP_NAME^^} :: Initializer for docker" + echo "________________________________________________________________________/ $_version" echo - echo " $( _key m ) - more infos" - echo " $( _key c ) - console (bash)" - echo - echo " $( _key q ) - quit" + + _showContainers + + h2 MENU + showMenu echo echo -n " select >" read -rn 1 action @@ -275,6 +698,8 @@ while true; do fi case "$action" in + "-h") showHelp; exit 0 ;; + "-v") echo "$_self $_version"; exit 0 ;; g) _removeGitdata ;; @@ -289,51 +714,96 @@ while true; do _removeGeneratedFiles rm -rf containers ;; - # not in the menu - # f) - # _removeGeneratedFiles - # _generateFiles - # _wait - # ;; m) _showInfos _wait ;; u|U) - dockerUp="docker-compose -p "$APP_NAME" --verbose up -d --remove-orphans" + h2 "Bring up..." + dockerUp="docker-compose -p $APP_NAME --verbose up -d --remove-orphans" if [ "$action" = "U" ]; then dockerUp+=" --build" fi + echo "$dockerUp" if $dockerUp; then - echo "In a web browser:" - echo " $frontendurl" + _showBrowserurl else echo "ERROR: docker-compose up failed :-/" docker-compose -p "$APP_NAME" logs | tail fi echo - _wait ;; s) + h2 "Stopping..." docker-compose -p "$APP_NAME" stop ;; r) + h2 "Removing..." docker-compose -p "$APP_NAME" rm -f ;; c) - docker ps - echo -n "id or name >" - read dockerid - test -z "$dockerid" || docker exec -it $dockerid /bin/bash + h2 "Console" + _containers=$( docker-compose -p "$APP_NAME" ps | sed -n "2,\$p" | awk '{ print $1}' ) + if [ "$DB_ADD" = "false" ]; then + dockerid=$_containers + else + echo "Select a container:" + sed "s#^# #g" <<< "$_containers" + echo -n "id or name >" + read -r dockerid + fi + test -z "$dockerid" || ( + echo + echo "> docker exec -it $dockerid /bin/bash (type 'exit' + Return when finished)" + docker exec -it "$dockerid" /bin/bash + ) + ;; + p) + h2 "PHP $APP_PHP_VERSION linter" + + dockerid="${APP_NAME}-server" + echo -n "Scanning ... " + typeset -i _iFiles + _iFiles=$( docker exec -it "$dockerid" /bin/bash -c "find . -name '*.php' " | wc -l ) + + if [ $_iFiles -gt 0 ]; then + echo "found $_iFiles [*.php] files ... errors from PHP $APP_PHP_VERSION linter:" + time if echo "$APP_PHP_VERSION" | grep -E "([567]\.|8\.[012])" >/dev/null ; then + docker exec -it "$dockerid" /bin/bash -c "find . -name '*.php' -exec php -l {} \; | grep -v '^No syntax errors detected'" + else + docker exec -it "$dockerid" /bin/bash -c "php -l \$( find . -name '*.php' ) | grep -v '^No syntax errors detected' " + fi + echo + _wait + else + echo "Start your docker container first." + fi + ;; + d) + h2 "DB tools :: dump" + _dbDump + ;; + D) + h2 "DB tools :: import" + _dbImport + ;; + M) + h2 "DB tools :: mysql client" + docker exec -it "${APP_NAME}-db" mysql -uroot -p${MYSQL_ROOT_PASS} "${MYSQL_DB}" + ;; + o) + h2 "Open app ..." + xdg-open "$DC_WEB_URL" ;; q) + h2 "Bye!" exit 0; ;; *) test -n "$action" && ( echo " ACTION FOR [$action] NOT IMPLEMENTED."; sleep 1 ) esac - action= + action=$1; shift 1 done diff --git a/docker/init.sh.cfg b/docker/init.sh.cfg index b96727ecfc897540f1dbbe911cd3293f12d74558..7b4f3fc422ab900d86819f4eb5ff514f7d318142 100644 --- a/docker/init.sh.cfg +++ b/docker/init.sh.cfg @@ -15,9 +15,9 @@ APP_PORT=8001 APP_APT_PACKAGES="git unzip zip libapache2-mod-xsendfile" #APP_APACHE_MODULES="rewrite" -APP_APACHE_MODULES="xsendfile" +APP_APACHE_MODULES="rewrite xsendfile" -APP_PHP_VERSION=8.2 +APP_PHP_VERSION=8.4 # APP_PHP_MODULES="curl pdo_mysql mbstring xml zip xdebug" APP_PHP_MODULES="" @@ -60,9 +60,8 @@ DOCKER_USER_UID=33 # document root inside web-server container WEBROOT=/var/www/${APP_NAME}/public_html +WEBURL="/" CUTTER_NO_DATABASE="CUT-HERE-FOR-NO-DATABASE" -frontendurl=http://localhost:${APP_PORT}/ - # ---------------------------------------------------------------------- diff --git a/hooks/templates/inc_config.php.erb b/hooks/templates/inc_config.php.erb index f9c97088e80d4b03072f1af417d3a996c615e082..cf727fe080b4301a1f6c0a4f4dabf2766b15472f 100644 --- a/hooks/templates/inc_config.php.erb +++ b/hooks/templates/inc_config.php.erb @@ -30,4 +30,10 @@ return array( // allow directory listing when accessing a path of a package // true is required to fetch all packages 'showdircontent'=>true, + + // --- monitoring + + // appmonitor ip that is allowed to fetch data + 'monitor-ips'=>[<%= @replace["monitorip"] %>], + ); diff --git a/public_html/appmonitor/check-appmonitor-server.php b/public_html/appmonitor/check-appmonitor-server.php new file mode 100644 index 0000000000000000000000000000000000000000..35bd349c19fbc23d7ad74f95f86ca27976a372a1 --- /dev/null +++ b/public_html/appmonitor/check-appmonitor-server.php @@ -0,0 +1,45 @@ +<?php +/* ______________________________________________________________________ + * + * A P P M O N I T O R :: CLIENT - CHECK + * ______________________________________________________________________ + * + * This is the check file for the appmonitor server installation + * Have a look to the docs/client-php.md and index.sample.php + * to write your own checks + * + * @author: Axel Hahn + * ---------------------------------------------------------------------- + * 2019-04-29 aded check for ssl cert; removed a check + * 2019-05-17 aded check http to config- and tmp dir + * 2021-11-nn removed all checks ... created as single files + * 2022-03-28 put checks into plugins/apps/ + */ + +$sApproot = str_replace('\\', '/', dirname(__DIR__)); + +require_once($sApproot.'/client/classes/appmonitor-client.class.php'); + +// require_once('classes/client_all_in_one.php'); +$oMonitor = new appmonitor(); +$oMonitor->setWebsite('Appmonitor server'); + +// how often the server should ask for updates +$oMonitor->setTTL(300); +$oMonitor->addTag('monitoring'); + + +// a general include ... the idea is to a file with the same actions on all +// installations and hosts that can be deployed by a software delivery service +// (Puppet, Ansible, ...) +@include 'general_include.php'; + +// include default checks for an application +@require 'plugins/apps/iml-appmonitor-server.php'; + +// ---------------------------------------------------------------------- + +$oMonitor->setResult(); +$oMonitor->render(); + +// ---------------------------------------------------------------------- diff --git a/public_html/appmonitor/classes/appmonitor-checks.class.php b/public_html/appmonitor/classes/appmonitor-checks.class.php new file mode 100755 index 0000000000000000000000000000000000000000..865a0e8e041ca1cc0f46d13c2db5aa8d4a8f2d37 --- /dev/null +++ b/public_html/appmonitor/classes/appmonitor-checks.class.php @@ -0,0 +1,423 @@ +<?php + +if (!defined('RESULT_OK')) { + define("RESULT_OK", 0); + define("RESULT_UNKNOWN", 1); + define("RESULT_WARNING", 2); + define("RESULT_ERROR", 3); +} + +/** + * ____________________________________________________________________________ + * + * _____ _____ __ _____ _ _ + * | | | | ___ ___ ___| |___ ___|_| |_ ___ ___ + * |- -| | | | |__ | .'| . | . | | | | . | | | _| . | _| + * |_____|_|_|_|_____| |__,| _| _|_|_|_|___|_|_|_|_| |___|_| + * |_| |_| + * _ _ _ + * ___| |_|___ ___| |_ + * | _| | | -_| | _| + * |___|_|_|___|_|_|_| + * + * ____________________________________________________________________________ + * + * APPMONITOR :: CLASS FOR CLIENT TEST FUNCTIONS<br> + * <br> + * THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE <br> + * LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR <br> + * OTHER PARTIES PROVIDE THE PROGRAM ?AS IS? WITHOUT WARRANTY OF ANY KIND, <br> + * EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED <br> + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE <br> + * ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. <br> + * SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY <br> + * SERVICING, REPAIR OR CORRECTION.<br> + * <br> + * --------------------------------------------------------------------------------<br> + * <br> + * --- HISTORY:<br> + * 2014-10-24 0.5 axel.hahn@iml.unibe.ch<br> + * 2015-04-08 0.9 axel.hahn@iml.unibe.ch added sochket test: checkPortTcp<br> + * 2018-06-29 0.24 axel.hahn@iml.unibe.ch add file and directory checks<br> + * 2018-07-17 0.42 axel.hahn@iml.unibe.ch add port on mysqli check<br> + * 2018-07-26 0.46 axel.hahn@iml.unibe.ch fix mysql connection check with empty port param<br> + * 2018-08-14 0.47 axel.hahn@iml.unibe.ch appmonitor client: use timeout of 5 sec for tcp socket connections<br> + * 2018-08-15 0.49 axel.hahn@iml.unibe.ch cert check: added flag to skip verification<br> + * 2018-08-23 0.50 axel.hahn@iml.unibe.ch replace mysqli connect with mysqli real connect (to use a timeout)<br> + * 2018-08-27 0.52 axel.hahn@iml.unibe.ch add pdo connect (starting with mysql)<br> + * 2018-11-05 0.58 axel.hahn@iml.unibe.ch additional flag in http check to show content<br> + * 2019-05-31 0.87 axel.hahn@iml.unibe.ch add timeout as param in connective checks (http, tcp, databases)<br> + * 2019-06-05 0.88 axel.hahn@iml.unibe.ch add plugins<br> + * 2021-10-28 0.93 axel.hahn@iml.unibe.ch add plugins<br> + * 2021-12-14 0.93 axel.hahn@iml.unibe.ch split plugins into single files; added key group in a check<br> + * 2023-06-02 0.125 axel.hahn@unibe.ch replace array_key_exists for better readability + * 2024-07-22 0.137 axel.hahn@unibe.ch php 8 only: use typed variables + * --------------------------------------------------------------------------------<br> + * @version 0.137 + * @author Axel Hahn + * @link TODO + * @license GPL + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL 3.0 + * @package IML-Appmonitor + */ +class appmonitorcheck +{ + // ---------------------------------------------------------------------- + // CONFIG + // ---------------------------------------------------------------------- + + /** + * starting time using microtime + * @var float + */ + protected float $_iStart = 0; + + /** + * config container + * @var array + */ + protected array $_aConfig = []; + + /** + * data of all checks + * @var array + */ + protected array $_aData = []; + + /** + * flat array with units for sizes + * @var array + */ + protected array $_units = ['B', 'KB', 'MB', 'GB', 'TB']; + + /** + * timeout in sec for tcp socket connections + * @var integer + */ + protected int $_iTimeoutTcp = 5; + + /** + * point to the plugin directory + * @var string + */ + protected string $_sPluginDir = __DIR__ . '/../plugins'; + + // ---------------------------------------------------------------------- + // CONSTRUCTOR + // ---------------------------------------------------------------------- + + /** + * Constructor (nothing here) + */ + public function __construct() + { + + } + + // ---------------------------------------------------------------------- + // PRIVATE FUNCTIONS + // ---------------------------------------------------------------------- + + /** + * Internal: create basic array values for metadata + * @return boolean + */ + protected function _createDefaultMetadata(): bool + { + + $this->_aData = [ + "name" => $this->_aConfig["name"], + "description" => $this->_aConfig["description"], + "group" => isset($this->_aConfig["group"]) ? $this->_aConfig["group"] : false, + "parent" => isset($this->_aConfig["parent"]) ? $this->_aConfig["parent"] : false, + "result" => RESULT_UNKNOWN, + "value" => false, + "type" => false, + "time" => false, + ]; + return true; + } + + /** + * Set the result value of a check + * @param integer $iResult result code; one of RESULT_OK|RESULT_WARNING|RESULT_ERROR|RESULT_UNKNOWN + * @return bool + */ + protected function _setResult(int $iResult): bool + { + $this->_aData["result"] = (int) $iResult; + return true; + } + + /** + * Set a result value of a check + * @param string $s value; message text for this result + * @return bool + */ + protected function _setOutput(string $s): bool + { + $this->_aData["value"] = $s; + return true; + } + + /** + * Put counter data to result set + * @param array $aParams array with possible keys type, count, visual + * @return boolean + */ + protected function _setCounter(array $aParams): bool + { + if (is_array($aParams) && count($aParams)) { + foreach (['type', 'count', 'visual'] as $sMyKey) { + if (isset($aParams[$sMyKey])) { + $this->_aData[$sMyKey] = $aParams[$sMyKey]; + } + } + } + return true; + } + + /** + * Set result and output + * @param integer $iResult result code; one of RESULT_OK|RESULT_WARNING|RESULT_ERROR|RESULT_UNKNOWN + * @param string $s message text + * @param array $aCounter optional: counter with array keys type, count, visual + * @return boolean + */ + protected function _setReturn(int $iResult, string $s, array $aCounter = []) + { + $this->_setResult($iResult); + $this->_setOutput($s); + $this->_setCounter($aCounter); + return true; + } + + /** + * Check a given array if it contains wanted keys + * @param array $aConfig array to verify + * @param string $sKeyList key or keys as comma seprated list + * @return boolean + */ + protected function _checkArrayKeys($aConfig, $sKeyList) + { + foreach (explode(",", $sKeyList) as $sKey) { + if (!isset($aConfig[$sKey])) { + header('HTTP/1.0 503 Service Unavailable'); + die('<h1>503 Service Unavailable</h1>' + . '<h2>Details</h2>' + . __METHOD__ . " - array of check parameters requires the keys [$sKeyList] - but key <code>$sKey</code> was not found in config array." + . "<pre>" . print_r($aConfig, true) . '</pre>' + ); + } + if (is_null($aConfig[$sKey])) { + header('HTTP/1.0 503 Service Unavailable'); + die('<h1>503 Service Unavailable</h1>' + . '<h2>Details</h2>' + . __METHOD__ . " - key <code>$sKey</code> is empty in config array" + . "<pre>" . print_r($aConfig, true) . '</pre>' + ); + } + } + return true; + } + + // ---------------------------------------------------------------------- + // PUBLIC FUNCTIONS + // ---------------------------------------------------------------------- + + /** + * Perform a check + * @param array $aConfig configuration array for a check, eg. + * <code> + * [ + * [name] => Dummy + * [description] => Dummy Test + * [check] => [ + * [function] => [check function] // i.e. Simple + * [params] => [array] // optional; arguments for Check function + * // its keys depend on the function + * ] + * ] + * </code> + * @return array + */ + public function makeCheck(array $aConfig): array + { + $this->_iStart = microtime(true); + $this->_checkArrayKeys($aConfig, "name,description,check"); + $this->_checkArrayKeys($aConfig["check"], "function"); + + $this->_aConfig = $aConfig; + $this->_createDefaultMetadata(); + + $sCheck = preg_replace('/[^a-zA-Z0-9]/', '', $this->_aConfig["check"]["function"]); + $aParams = $this->_aConfig["check"]["params"] ?? []; + + // try to load as plugin from a plugin file + $sPluginFile = strtolower($this->_sPluginDir . '/checks/' . $sCheck . '.php'); + // echo "plugin file: $sPluginFile<br>\n"; + $sCheckClass = 'check' . $sCheck; + if (!class_exists($sCheckClass)) { + if (file_exists($sPluginFile)) { + require_once ($sPluginFile); + } + } + + if (!class_exists($sCheckClass)) { + header('HTTP/1.0 503 Service Unavailable'); + die('<h1>503 Service Unavailable</h1>' + . '<h2>Details</h2>' + . __METHOD__ . " - check class not found: <code>$sCheckClass</code>" + . "<pre>" . print_r($aConfig, true) . '</pre>' + . "<h2>Known checks</h2>\n" . print_r($this->listChecks(), 1) + ); + } + + $oPlugin = new $sCheckClass; + $aResponse = $oPlugin->run($aParams); + if (!is_array($aResponse)) { + header('HTTP/1.0 503 Service Unavailable'); + die('<h1>503 Service Unavailable</h1>' + . '<h2>Details</h2>' + . __METHOD__ . " - plugin : $sCheck does not responses an array" + . "<pre>INPUT " . print_r($aConfig, true) . '</pre>' + . "<pre>RESPONSE " . print_r($aResponse, true) . '</pre>' + ); + } + if (count($aResponse) < 2) { + header('HTTP/1.0 503 Service Unavailable'); + die('<h1>503 Service Unavailable</h1>' + . '<h2>Details</h2>' + . __METHOD__ . " - plugin : $sCheck does not responses the minimum of 2 array values" + . "<pre>INPUT " . print_r($aConfig, true) . '</pre>' + . "<pre>RESPONSE " . print_r($aResponse, true) . '</pre>' + ); + } + if (!isset($aResponse[2]) || !$aResponse[2]) { + $aResponse[2] = []; + } + $this->_setReturn($aResponse[0], $aResponse[1], $aResponse[2]); + if (!$this->_aData['group'] && method_exists($oPlugin, "getGroup")) { + $this->_aData['group'] = $oPlugin->getGroup($aParams); + } + + $this->_aData['time'] = number_format((microtime(true) - $this->_iStart) * 1000, 3) . 'ms'; + // ... and send response + return $this->respond(); + } + + /** + * List all available checks. This is a helper class you can call + * to get an overview over built in functions and plugins. + * You get a flat array with all function names. + * @return array + */ + public function listChecks(): array + { + $aReturn = []; + // return internal protected fuctions named "check[whatever]" + $class = new ReflectionClass($this); + foreach ($class->getMethods(ReflectionMethod::IS_PROTECTED) as $oReflectionMethod) { + if (strpos($oReflectionMethod->name, "check") === 0) { + $aReturn[(string) $oReflectionMethod->name] = 1; + } + } + // return checks from plugins subdir + foreach (glob($this->_sPluginDir . '/checks/*.php') as $sPluginFile) { + $aReturn[str_replace('.php', '', basename($sPluginFile))] = 1; + } + ksort($aReturn); + return array_keys($aReturn); + } + + /** + * Final call of class: send response (data array) + * @return array + */ + public function respond() + { + return $this->_aData; + } + + // ---------------------------------------------------------------------- + // CHECK FUNCTIONS (protected) + // ---------------------------------------------------------------------- + + /** + * Helper function: read certificate data + * called in checkCert() + * + * @param string $sUrl url to connect + * @param boolean $bVerifyCert flag: verify certificate; default: no check + * @return array + */ + protected function _certGetInfos(string $sUrl, bool $bVerifyCert): array + { + $iTimeout = 10; + $aUrldata = parse_url($sUrl); + $sHost = isset($aUrldata['host']) ? $aUrldata['host'] : false; + $iPort = isset($aUrldata['port']) ? $aUrldata['port'] : ((isset($aUrldata['scheme']) && $aUrldata['scheme'] === 'https') ? 443 : false); + + $aSsl = ['capture_peer_cert' => true]; + if ($bVerifyCert) { + $aSsl['verify_peer'] = false; + $aSsl['verify_peer_name'] = false; + } + ; + $get = stream_context_create(['ssl' => $aSsl]); + if (!$get) { + return ['_error' => 'Error: Cannot create stream_context']; + } + $errno = -1; + $errstr = "stream_socket_client failed."; + $read = stream_socket_client("ssl://$sHost:$iPort", $errno, $errstr, $iTimeout, STREAM_CLIENT_CONNECT, $get); + if (!$read) { + return ['_error' => "Error $errno: $errstr; cannot create stream_socket_client with given stream_context to ssl://$sHost:$iPort; you can try to set the flag [verify] to false to check expiration date only."]; + } + $cert = stream_context_get_params($read); + if (!$cert) { + return ['_error' => "Error: socket was connected to ssl://$sHost:$iPort - but I cannot read certificate infos with stream_context_get_params "]; + } + return openssl_x509_parse($cert['options']['ssl']['peer_certificate']); + } + + + /** + * Get human readable space value + * @param integer $size + * @return string + */ + protected function _getHrSize(int $size): string + { + $power = $size > 0 ? floor(log($size, 1024)) : 0; + return number_format($size / pow(1024, $power), 2, '.', ',') . ' ' . $this->_units[$power]; + } + + /** + * get a space in a real value if an integer has added MB|GB|... + * @param string $sValue + * @return integer + */ + protected function _getSize(string $sValue): int + { + if (is_int($sValue)) { + return $sValue; + } + $power = 0; + foreach ($this->_units as $sUnit) { + if (preg_match('/^[0-9\.\ ]*' . $sUnit . '/', $sValue)) { + $i = preg_replace('/([0-9\.]*).*/', '$1', $sValue); + $iReal = $i * pow(1024, $power); + // die("FOUND: $sValue with unit ${sUnit} - 1024^$power * $i = $iReal"); + return $iReal; + } + $power++; + } + header('HTTP/1.0 503 Service Unavailable'); + die('<h1>503 Service Unavailable</h1>' + . '<h2>Details</h2>' + . __METHOD__ . " ERROR in space value parameter - there is no size unit in [$sValue] - allowed size units are " . implode('|', $this->_units) + ); + } + +} diff --git a/public_html/appmonitor/classes/appmonitor-client.class.php b/public_html/appmonitor/classes/appmonitor-client.class.php new file mode 100755 index 0000000000000000000000000000000000000000..66def5a5d02705512751a93982d325823d7a35f3 --- /dev/null +++ b/public_html/appmonitor/classes/appmonitor-client.class.php @@ -0,0 +1,549 @@ +<?php +if (!class_exists('appmonitorcheck')) { + require_once 'appmonitor-checks.class.php'; +} + +/** + * ____________________________________________________________________________ + * + * _____ _____ __ _____ _ _ + * | | | | ___ ___ ___| |___ ___|_| |_ ___ ___ + * |- -| | | | |__ | .'| . | . | | | | . | | | _| . | _| + * |_____|_|_|_|_____| |__,| _| _|_|_|_|___|_|_|_|_| |___|_| + * |_| |_| + * _ _ _ + * ___| |_|___ ___| |_ + * | _| | | -_| | _| + * |___|_|_|___|_|_|_| + * + * ____________________________________________________________________________ + * + * APPMONITOR :: CLASS FOR CLIENT CHECKS<br> + * <br> + * THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE <br> + * LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR <br> + * OTHER PARTIES PROVIDE THE PROGRAM ?AS IS? WITHOUT WARRANTY OF ANY KIND, <br> + * EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED <br> + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE <br> + * ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. <br> + * SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY <br> + * SERVICING, REPAIR OR CORRECTION.<br> + * <br> + * --------------------------------------------------------------------------------<br> + * <br> + * --- HISTORY:<br> + * 2014-10-24 0.5 axel.hahn@iml.unibe.ch<br> + * 2014-11-21 0.6 axel.hahn@iml.unibe.ch removed meta::ts <br> + * 2018-08-23 0.50 axel.hahn@iml.unibe.ch show version<br> + * 2018-08-24 0.51 axel.hahn@iml.unibe.ch method to show local status page<br> + * 2018-08-27 0.52 axel.hahn@iml.unibe.ch add pdo connect (starting with mysql)<br> + * 2018-11-05 0.58 axel.hahn@iml.unibe.ch additional flag in http check to show content<br> + * 2019-05-31 0.87 axel.hahn@iml.unibe.ch add timeout as param in connective checks (http, tcp, databases)<br> + * 2020-05-03 0.110 axel.hahn@iml.unibe.ch update renderHtmloutput<br> + * 2023-07-06 0.128 axel.hahn@unibe.ch update httpcontent check<br> + * 2024-07-19 0.137 axel.hahn@unibe.ch php 8 only: use typed variables + * 2024-11-22 0.141 axel.hahn@unibe.ch Set client version to server version after updating http, mysqli and app checks + * 2025-01-02 0.149 axel.hahn@unibe.ch add getChecks method + * --------------------------------------------------------------------------------<br> + * @version 0.149 + * @author Axel Hahn + * @link TODO + * @license GPL + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL 3.0 + * @package IML-Appmonitor + */ +class appmonitor +{ + + /** + * Name and Version number + * @var string + */ + protected string $_sVersion = 'php-client-v0.149'; + + /** + * config: default ttl for server before requesting the client check again + * value is in seconds + * @var int + */ + protected int $_iDefaultTtl = 300; + + /** + * internal counter: greatest return value of all checks + * @var integer + */ + protected int $_iMaxResult = -1; + + /** + * responded metadata of a website + * @see _createDefaultMetadata() + * @var array + */ + protected array $_aMeta = []; + + /** + * Response array of all checks + * @see addCheck() + * @var array + */ + protected array $_aChecks = []; + + /** + * for time measurements: start time + * @var float + */ + protected float $_iStart = 0; + + /** + * constructor: init data + */ + public function __construct() + { + $this->_createDefaultMetadata(); + } + + // ---------------------------------------------------------------------- + // protected function + // ---------------------------------------------------------------------- + + /** + * Create basic array values for metadata + * @return boolean + */ + protected function _createDefaultMetadata(): bool + { + $this->_iStart = microtime(true); + $this->_aMeta = [ + "host" => false, + "website" => false, + "ttl" => false, + "result" => false, + "time" => false, + "version" => $this->_sVersion, + ]; + + // fill with default values + $this->setHost(); + $this->setWebsite(); + $this->setTTL(); + return true; + } + + // ---------------------------------------------------------------------- + // setter + // ---------------------------------------------------------------------- + + /** + * Set the physical hostname for metadata; if no host is given then + * the php_uname("n") will be used to set one. + * + * @param string $s hostname + * @return bool + */ + public function setHost(string $s = ''): bool + { + if (!$s) { + $s = php_uname("n"); + } + if (!$s) { + return false; + } + $this->_aMeta["host"] = $s; + return true; + } + + /** + * Set a name for this website or application and its environment + * (dev, test, prod); + * + * If you have several application in subdirectories, i.e. /blog, /shop... + * then you should the path or any description to identify them too + * + * if no argument is given the name of HTTP_HOST will be used + * + * @param string $sWebsite Name of the website or web application + * @return boolean + */ + public function setWebsite($sWebsite = ''): bool + { + if (!$sWebsite && isset($_SERVER["HTTP_HOST"])) { + $sWebsite = $_SERVER["HTTP_HOST"]; + } + if (!$sWebsite) { + return false; + } + $this->_aMeta["website"] = $sWebsite; + return true; + } + + /** + * set a ttl value in seconds to define how long a server should not + * ask again for a new status of this instance + * + * @param int $iTTl TTL value in sec + * @return boolean + */ + public function setTTL($iTTl = 0) + { + if ($iTTl == 0) { + $iTTl = $this->_iDefaultTtl; + } + return $this->_aMeta["ttl"] = $iTTl; + } + + /** + * Set final result in meta data; if no value was given then it + * sets the biggest value of any check. + * + * @param integer $iResult set resultcode; one of RESULT_OK|RESULT_WARNING|RESULT_ERROR|RESULT_UNKNOWN + * @return boolean + */ + public function setResult(int $iResult = -1): bool + { + if ($iResult === -1) { + $iResult = $this->_iMaxResult; // see addCheck() + } + $this->_aMeta["result"] = $iResult; + return true; + } + + /** + * Add a check array + * @param array $aJob array with check data + * @return boolean + */ + public function addCheck($aJob = []): bool + { + + require_once 'appmonitor-checks.class.php'; + $oCheck = new appmonitorcheck(); + $aCheck = $oCheck->makecheck($aJob); + + // limit result code + $iMyResult = isset($aJob['worstresult']) + ? min($aCheck["result"], $aJob['worstresult']) + : $aCheck["result"] + ; + + if (!$this->_iMaxResult || $iMyResult > $this->_iMaxResult) { + $this->_iMaxResult = $iMyResult; + } + $this->_aChecks[] = $aCheck; + return true; + } + + /** + * Add an item to notifications meta data + * @see addEmail() + * @see addSlack() + * + * @param string $sType type ... one of email|slack + * @param string $sValue value + * @param string $sKey optional key (for key->value instead of list of values) + * @return boolean + */ + protected function _addNotification(string $sType, string $sValue, string $sKey = ''): bool + { + $sTypeCleaned = preg_replace('/[^a-z]/', '', strtolower($sType)); + if (!isset($this->_aMeta['notifications'])) { + $this->_aMeta['notifications'] = []; + } + if (!isset($this->_aMeta['notifications'][$sTypeCleaned])) { + $this->_aMeta['notifications'][$sTypeCleaned] = []; + } + if ($sKey) { + $this->_aMeta['notifications'][$sTypeCleaned][$sKey] = $sValue; + } else { + $this->_aMeta['notifications'][$sTypeCleaned][] = $sValue; + } + return true; + } + + /** + * Add an email to notifications list + * + * @param string $sEmailAddress email address to add + * @return boolean + */ + public function addEmail(string $sEmailAddress) + { + return $this->_addNotification('email', $sEmailAddress); + } + + /** + * Add slack channel for notification + * @param string $sLabel + * @param string $sSlackWebhookUrl + * @return boolean + */ + public function addSlackWebhook(string $sLabel, string $sSlackWebhookUrl): bool + { + return $this->_addNotification('slack', $sSlackWebhookUrl, $sLabel); + } + + /** + * Add a tag for grouping in the server gui. + * Spaces will be replaced with underscore + * + * @param string $sTag tag to add + * @return boolean + */ + public function addTag(string $sTag): bool + { + if (!isset($this->_aMeta['tags'])) { + $this->_aMeta['tags'] = []; + } + $this->_aMeta['tags'][] = str_replace(' ', '_', $sTag); + return true; + } + + /** + * Check referers IP address if it matches any entry in the list + * requires http request; CLI is always allowed + * On deny this method exits with 403 response + * + * @param array $aAllowedIps array of allowed ip addresses / ranges + * the ip must match from the beginning, i.e. + * "127.0." will allow requests from 127.0.X.Y + * @return boolean + */ + public function checkIp(array $aAllowedIps = []): bool + { + if (!isset($_SERVER['REMOTE_ADDR']) || !count($aAllowedIps)) { + return true; + } + $sIP = $_SERVER['REMOTE_ADDR']; + foreach ($aAllowedIps as $sIp2Check) { + if (strpos($sIP, $sIp2Check) === 0) { + return true; + } + } + header('HTTP/1.0 403 Forbidden'); + die('ERROR: Your ip address [' . $sIP . '] has no access.'); + } + + /** + * Check a token + * requires http request; CLI is always allowed + * On deny this method exits with 403 response + * + * @param string $sVarname name of GET variable + * @param string $sToken value + * @return boolean + */ + public function checkToken(string $sVarname, string $sToken): bool + { + if (!isset($_GET)) { + return true; + } + if (isset($_GET[$sVarname]) && $_GET[$sVarname] === $sToken) { + return true; + } + header('HTTP/1.0 403 Forbidden'); + die('ERROR: A token is required.'); + } + + // ---------------------------------------------------------------------- + // getter + // ---------------------------------------------------------------------- + + /** + * list all available check functions. This is a helper class you cann call + * to get an overview over built in functions. You get a flat array with + * all function names. + * @return array + */ + public function listChecks(): array + { + require_once 'appmonitor-checks.class.php'; + $oCheck = new appmonitorcheck(); + return $oCheck->listChecks(); + } + + // ---------------------------------------------------------------------- + // checks + // ---------------------------------------------------------------------- + + /** + * verify array values and in case of an error abort and show all found errors + * @return boolean + */ + protected function _checkData(): bool + { + $aErrors = []; + + if (!count($this->_aChecks)) { + $aErrors[] = "No checks have been defined."; + } + + if ($this->_aMeta["result"] === false) { + $aErrors[] = "method setResult was not used to set a final result for all checks."; + } + + if (count($aErrors)) { + $this->abort( + '<h2>Error: client check is not complete</h2><p>Found errors:</p><ol><li>' . implode('<li>', $aErrors) . '</ol><br><br>' + // .'Dump of your data so far:<pre>' . json_encode($this->getResults(), JSON_PRETTY_PRINT) . '</pre><hr>' + ); + } + return true; + } + + // ---------------------------------------------------------------------- + // output + // ---------------------------------------------------------------------- + + /** + * Stop processing the client checks and abort with an error + * @param string $sMessage text to show after a 503 headline + * @return void + */ + public function abort(string $sMessage): void + { + header('HTTP/1.0 503 Service Unavailable'); + die('<h1>503 Service Unavailable</h1>' . $sMessage); + } + + /** + * Get array with executed checks + * @return array + */ + public function getChecks(): array + { + return $this->_aChecks; + } + + /** + * Get full array for response with metadata and checks + * @return array + */ + public function getResults(): array + { + return [ + "meta" => $this->_aMeta, + "checks" => $this->_aChecks, + ]; + } + + /** + * Send http response with header and appmonitor JSON data + * @return string + */ + public function render(): string + { + $this->_checkData(); + $this->_aMeta['time'] = number_format((microtime(true) - $this->_iStart) * 1000, 3) . 'ms'; + $sOut=json_encode($this->getResults()); + + header('Content-type: application/json'); + header('Cache-Control: cache'); + header('max-age: ' . $this->_aMeta["ttl"]); + echo $sOut; + return $sOut; + } + + /** + * Output appmonitor client status as single html page + * + * @example <code> + * ob_start();<br> + * require __DIR__ . '/appmonitor/client/index.php'; + * $sJson=ob_get_contents(); + * ob_end_clean(); + * $oMonitor->renderHtmloutput($sJson); + * </code> + * + * @param string $sJson JSON of client output + * @return string + */ + public function renderHtmloutput(string $sJson): string + { + + header('Content-type: text/html'); + header('Cache-Control: cache'); + header('max-age: ' . $this->_aMeta["ttl"]); + $aMsg = [ + 0 => "OK", + 1 => "UNKNOWN", + 2 => "WARNING", + 3 => "ERROR" + ]; + + // $sOut = print_r($sJson, 1); + $aData = json_decode($sJson, 1); + + // ----- Ausgabe human readable + $sOut = ''; + $sOut .= '' + . '<h2>Metadata</h2>' + . '<div class="meta' . (isset($aData['meta']['result']) ? ' result' . $aData['meta']['result'] : '') . '">' + . 'Status: ' . (isset($aData['meta']['result']) ? $aMsg[$aData['meta']['result']] : '?') . '<br>' + . '</div>' + . 'Host: ' . (isset($aData['meta']['host']) ? '<span class="string">' . $aData['meta']['host'] . '</span>' : '?') . '<br>' + . 'Website: ' . (isset($aData['meta']['website']) ? '<span class="string">' . $aData['meta']['website'] . '</span>' : '?') . '<br>' + . 'Execution time: ' . (isset($aData['meta']['time']) ? '<span class="float">' . $aData['meta']['time'] . '</span>' : '?') . '<br>' + . 'Client: ' . (isset($aData['meta']['version']) ? '<span class="string">' . $aData['meta']['version'] . '</span>' : '?') . '<br>' + + . '<h2>Checks</h2>' + ; + if (isset($aData['checks'][0]) && count($aData['checks'])) { + foreach ($aData['checks'] as $aCheck) { + $sOut .= '' + . '<span class="result' . $aCheck['result'] . '"> <strong>' . $aCheck['name'] . '</strong></span> <br>' + . '<div class="check">' + . '<div class="description">' + . $aCheck['description'] . '<br>' + . $aCheck['value'] . '<br>' + . '</div>' + . 'Execution time: <span class="float">' . (isset($aCheck['time']) ? $aCheck['time'] : ' - ') . '</span><br>' + . 'Group: <span class="string">' . (isset($aCheck['group']) ? $aCheck['group'] : '-') . '</span><br>' + . 'parent: <span class="string">' . (isset($aCheck['parent']) ? $aCheck['parent'] : '-') . '</span><br>' + . 'Status: ' . $aMsg[$aCheck['result']] . '<br>' + . '</div>' + ; + } + } + $sOut .= '<h2>List of farbcodes</h2>'; + foreach ($aMsg as $i => $sText) { + $sOut .= '<span class="result' . $i . '">' . $sText . '</span> '; + } + + $sRaw=json_encode($aData, JSON_PRETTY_PRINT); + $sRaw = preg_replace('/:\ \"(.*)\"/U', ': "<span class="string">$1</span>"', $sRaw); + $sRaw = preg_replace('/:\ ([0-9]*)/', ': <span class="int">$1</span>', $sRaw); + $sRaw = preg_replace('/\"(.*)\":/U', '"<span class="key">$1</span>":', $sRaw); + + $sOut .= '<h2>Raw result data</h2><pre id="raw">' . $sRaw . '</pre>'; + + + $sOut = '<!DOCTYPE html><html><head>' + . '<style>' + . 'body{background:#eee; color:#444; font-family: verdana,arial; margin: 0; }' + . 'body>div#content{background: #fff; border-radius: 2em; border: 4px solid #abc; box-shadow: 0.5em 0.5em 2em #aaa; margin: 2em 10%; padding: 2em;}' + . 'h1{color:#346; margin: 0;}' + . 'h2{color:#569; margin-top: 2em;}' + . 'pre{background:#f4f4f8; padding: 1em; overflow-x:auto; }' + . '#raw .key{color:#808;}' + . '#raw .int{color:#3a3; font-weight: bold;}' + . '#raw .string{color:#66e;}' + . '.check{border: 1px solid #ccc; padding: 0.4em; margin-bottom: 2em;}' + . '.description{font-style: italic; padding: 0.4em 1em;}' + . '.float{color:#080;}' + . '.meta{margin-bottom: 1em;}' + . '.result0{background:#aca; border-left: 1em solid #080; padding: 0.5em; }' + . '.result1{background:#ccc; border-left: 1em solid #aaa; padding: 0.5em; }' + . '.result2{background:#fc9; border-left: 1em solid #860; padding: 0.5em; }' + . '.result3{background:#f88; border-left: 1em solid #f00; padding: 0.5em; }' + . '.string{color:#338;}' + . '</style>' + . '<title>' . __CLASS__ . '</title>' + . '</head><body>' + . '<div id="content">' + . '<h1>' . __CLASS__ . ' :: client status</h1>' + . $sOut + . '</div>' + . '</body></html>'; + return $sOut; + } + +} diff --git a/public_html/appmonitor/classes/client_all_in_one.php b/public_html/appmonitor/classes/client_all_in_one.php new file mode 100644 index 0000000000000000000000000000000000000000..b17aa09c49e0bb45388f3a6d382a66de4b644119 --- /dev/null +++ b/public_html/appmonitor/classes/client_all_in_one.php @@ -0,0 +1,26 @@ +<?php +/* + + MERGED APPMONITOR CLIENT :: WORK IN PROGRESS + + generated Tue Jul 23 16:47:01 CEST 2024 + +*/ + + class checkApacheProcesses extends appmonitorcheck { protected string $_sServerStatusUrl = 'http://localhost/server-status'; protected float $_iWarn = 50; protected float $_iError = 75; public function explain(): array { return [ 'name' => 'Plugin ApacheProcesses', 'descriptionm' => 'Check count running Apache processes', 'parameters' => [ 'url' => [ 'type' => 'string', 'required' => false, 'decsription' => 'Override https server-status page; default is http://localhost/server-status; Use it if the protocol to localhost is not http, but https or if it requires an authentication', 'default' => $this->_sServerStatusUrl, 'example' => '', ], 'warning' => [ 'type' => 'float', 'required' => false, 'decsription' => 'Limit to switch to warning (in percent)', 'default' => $this->_iWarn, 'example' => 30, ], 'error' => [ 'type' => 'float', 'required' => false, 'decsription' => 'Limit to switch to critical (in percent)', 'default' => $this->_iError, 'example' => 50, ], ], ]; } protected function _getApacheProcesses(): bool|array { $sBody = file_get_contents($this->_sServerStatusUrl); if (!$sBody) { return false; } $sRegexScoreboard = '/<pre>(.*)\<\/pre\>/U'; $aScore = []; $sStatusNobr = str_replace("\n", "", $sBody); if (preg_match_all($sRegexScoreboard, $sStatusNobr, $aTmpTable)) { $sScoreString = $aTmpTable[1][0]; $aScore['total'] = strlen($sScoreString); $aScore['free'] = substr_count($sScoreString, '.'); $aScore['waiting'] = substr_count($sScoreString, '_'); $aScore['active'] = $aScore['total'] - $aScore['free'] - $aScore['waiting']; } return $aScore; } public function getGroup(): string { return 'monitor'; } public function run(array $aParams): array { if (isset($aParams['url']) && $aParams['url']) { $this->_sServerStatusUrl = $aParams['url']; } if (isset($aParams['warning']) && (int) $aParams['warning']) { $this->_iWarn = (int) $aParams['warning']; } if (isset($aParams['error']) && (int) $aParams['error']) { $this->_iError = (int) $aParams['error']; } $aProcesses = $this->_getApacheProcesses(); $iActive = $aProcesses ? $aProcesses['active'] : false; if ($iActive === false) { $iResult = RESULT_UNKNOWN; } else { $sComment = ''; $iTotal = $aProcesses['total']; $iResult = RESULT_OK; if (($iActive / $iTotal * 100) > $this->_iWarn) { $iResult = RESULT_WARNING; $sComment = "more than warning level $this->_iWarn %"; } else { $sComment = "less than warning level $this->_iWarn %"; } if (($iActive / $iTotal * 100) > $this->_iError) { $iResult = RESULT_ERROR; $sComment = "more than error level $this->_iError %"; } } return [ $iResult, ($iActive === false ? 'Apache httpd server status is not available' : 'apache processes: ' . print_r($aProcesses, 1)) . ' ' . $sComment, ($iActive === false ? [] : [ 'type' => 'counter', 'count' => $iActive, 'visual' => 'line', ] ) ]; } } + class checkCert extends appmonitorcheck { public function getGroup(): string { return 'security'; } public function run(array $aParams): array { $sUrl = $aParams["url"] ?? 'http' . ($_SERVER['HTTPS'] ? 's' : '') . '://' . $_SERVER['SERVER_NAME'] . ':' . $_SERVER['SERVER_PORT']; $bVerify = isset($aParams["verify"]) ? !!$aParams["verify"] : true; $iWarn = isset($aParams["warning"]) ? (int) ($aParams["warning"]) : 21; $iCrtitcal = isset($aParams["critical"]) ? (int) ($aParams["critical"]) : 5; $sMessage = "Checked url: $sUrl ... "; $certinfo = $this->_certGetInfos($sUrl, $bVerify); if (isset($certinfo['_error'])) { return [ RESULT_ERROR, $certinfo['_error'] . $sMessage ]; } $sDNS = $certinfo['extensions']['subjectAltName'] ?? false; $sHost = parse_url($sUrl, PHP_URL_HOST); if (strstr($sDNS, "DNS:$sHost") === false) { return [ RESULT_ERROR, "Wrong certificate: $sHost is not listed as DNS alias in [$sDNS]. $sMessage" ]; } $iDaysleft = round(($certinfo['validTo_time_t'] - date('U')) / 60 / 60 / 24); $sMessage .= 'Issuer: ' . $certinfo['issuer']['O'] . '; valid from: ' . date("Y-m-d H:i", $certinfo['validFrom_time_t']) . ' to ' . date("Y-m-d H:i", $certinfo['validTo_time_t']) . ' ' . ($iDaysleft ? "($iDaysleft days left)" : "expired since " . (-$iDaysleft) . " days.") ; if ($iDaysleft <= 0) { return [ RESULT_ERROR, 'Expired! ' . $sMessage ]; } if ($iDaysleft <= $iWarn) { return [ RESULT_WARNING, ($iDaysleft <= $iCrtitcal ? 'Expires very soon! ' : 'Expires soon. ' ) . $sMessage ]; } return [ RESULT_OK, 'OK. ' . ($bVerify ? 'Certificate is valid. ' : '(Verification is disabled; Check for expiration only.) ') . $sMessage ]; } } + class checkDiskfree extends appmonitorcheck { public function getGroup(): string { return 'disk'; } public function run(array $aParams): array { $this->_checkArrayKeys($aParams, "directory,critical"); $sDirectory = $aParams["directory"]; if (!is_dir($sDirectory)) { return [ RESULT_ERROR, "directory [$sDirectory] does not exist. Maybe it is wrong or is not mounted." ]; } $iWarn = isset($aParams["warning"]) ? $this->_getSize($aParams["warning"]) : false; $iCritical = $this->_getSize($aParams["critical"]); $iSpaceLeft = disk_free_space($sDirectory); $sMessage = '[' . $sDirectory . '] has ' . $this->_getHrSize($iSpaceLeft) . ' left.'; if ($iWarn) { if ($iWarn <= $iCritical) { header('HTTP/1.0 503 Service Unavailable'); die("ERROR in a Diskfree check - warning value must be larger than critical.<pre>" . print_r($aParams, true)); } if ($iWarn < $iSpaceLeft) { return [ RESULT_OK, "$sMessage Warning level is not reached yet (still " . $this->_getHrSize($iSpaceLeft - $iWarn) . "over warning limit)." ]; } if ($iWarn > $iSpaceLeft && $iCritical < $iSpaceLeft) { return [ RESULT_WARNING, $sMessage . ' Warning level ' . $this->_getHrSize($iWarn) . ' was reached (space is ' . $this->_getHrSize($iWarn - $iSpaceLeft) . ' below warning limit; still ' . $this->_getHrSize($iSpaceLeft - $iCritical) . ' over critical limit).' ]; } } if ($iCritical < $iSpaceLeft) { return [RESULT_OK, $sMessage . ' Minimum is not reached yet (still ' . $this->_getHrSize($iSpaceLeft - $iCritical) . ' over critical limit).']; } else { return [RESULT_ERROR, $sMessage]; } } } + class checkExec extends appmonitorcheck { public function getGroup() { return 'service'; } public function run(array $aParams): array { $this->_checkArrayKeys($aParams, "command"); $_sCmd = $aParams['command']; $_bShowOutput = isset($aParams['output']) ? !!$aParams['output'] : true; $_aRcOK = isset($aParams['exitOK']) ? $aParams['exitOK'] : []; $_aRcWarning = isset($aParams['exitWarn']) ? $aParams['exitWarn'] : []; $_aRcCritical = isset($aParams['exitCritical']) ? $aParams['exitCritical'] : []; $_sMode = 'default'; if (count($_aRcOK) + count($_aRcWarning) + count($_aRcCritical)) { $_sMode = 'exitcode'; } exec($_sCmd, $aOutput, $iRc); $_sOut = $_bShowOutput ? '<br>' . implode("<br>", $aOutput) : ''; switch ($_sMode) { case "default": if ($iRc) { return [ RESULT_ERROR, 'command failed with exitcode ' . $iRc . ': [' . $_sCmd . ']' . $_sOut ]; } else { return [ RESULT_OK, "OK [$_sCmd] $_sOut" ]; } ; ; case "exitcode": if (in_array($iRc, $_aRcCritical)) { return [ RESULT_ERROR, "Critical exitcode $iRc detected: [$_sCmd] $_sOut" ]; } if (in_array($iRc, $_aRcWarning)) { return [ RESULT_WARNING, "Warning exitcode $iRc detected: [$_sCmd] $_sOut" ]; } if ($iRc == 0 || in_array($iRc, $_aRcOK)) { return [ RESULT_OK, "OK exitcode $iRc detected: [$_sCmd] $_sOut" ]; } return [ RESULT_UNKNOWN, "UNKNOWN - unhandled exitcode $iRc detected: [$_sCmd] $_sOut" ]; case "search": return [ RESULT_UNKNOWN, "UNKNOWN method [$_sMode] - is not implemented yet." ]; ; default: return [ RESULT_UNKNOWN, 'UNKNOWN mode [' . htmlentities($_sMode) . '].' ]; } } } + class checkFile extends appmonitorcheck { public function getGroup(array $aParams = []): string { $sReturn = 'file'; if (isset($aParams['dir'])) { $sReturn = 'folder'; } foreach (['exists', 'executable', 'readable', 'writable'] as $sFlag) { if (isset($aParams[$sFlag]) && !$aParams[$sFlag]) { $sReturn = 'deny'; } } return $sReturn; } public function run(array $aParams): array { $aOK = []; $aErrors = []; $this->_checkArrayKeys($aParams, "filename"); $sFile = $aParams["filename"]; if (isset($aParams['exists'])) { $sMyflag = 'exists=' . ($aParams['exists'] ? 'yes' : 'no'); if (file_exists($sFile) && $aParams['exists']) { $aOK[] = $sMyflag; } else { $aErrors[] = $sMyflag; } } foreach (['dir', 'executable', 'file', 'link', 'readable', 'writable'] as $sFiletest) { if (isset($aParams[$sFiletest])) { $sTestCmd = 'return is_' . $sFiletest . '("' . $sFile . '");'; if (eval ($sTestCmd) && $aParams[$sFiletest]) { $aOK[] = $sFiletest . '=' . ($aParams[$sFiletest] ? 'yes' : 'no'); } else { $aErrors[] = $sFiletest . '=' . ($aParams[$sFiletest] ? 'yes' : 'no'); } } } $sMessage = (count($aOK) ? ' flags OK: ' . implode('|', $aOK) : '') . ' ' . (count($aErrors) ? ' flags FAILED: ' . implode('|', $aErrors) : '') ; if (count($aErrors)) { return [ RESULT_ERROR, "file test [$sFile] $sMessage" ]; } else { return [ RESULT_OK, "file test [$sFile] $sMessage" ]; } } } + class checkHello extends appmonitorcheck { public function run(array $aParams): array { $this->_checkArrayKeys($aParams, "message"); return [ RESULT_OK, 'Hello world! My message is: ' . $aParams['message'] ]; } } + class checkHttpContent extends appmonitorcheck { public function getGroup(array $aParams=[]): string { $sReturn = 'service'; if (isset($aParams['status']) && $aParams['status'] > 300 && $aParams['status'] < 500) { $sReturn = 'deny'; } return $sReturn; } public function run(array $aParams) { $this->_checkArrayKeys($aParams, "url"); if (!function_exists("curl_init")) { header('HTTP/1.0 503 Service Unavailable'); die("ERROR: PHP CURL module is not installed."); } $bShowContent = (isset($aParams["content"]) && $aParams["content"]) ? true : false; $ch = curl_init($aParams["url"]); curl_setopt($ch, CURLOPT_HEADER, 1); curl_setopt($ch, CURLOPT_NOBODY, isset($aParams["headeronly"]) && $aParams["headeronly"]); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, isset($aParams["follow"]) && $aParams["follow"]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, isset($aParams["sslverify"]) ? !!$aParams["sslverify"] : 1); curl_setopt($ch, CURLOPT_TIMEOUT, (isset($aParams["timeout"]) && (int) $aParams["timeout"]) ? (int) $aParams["timeout"] : $this->_iTimeoutTcp); if (isset($aParams["userpwd"])) { curl_setopt($ch, CURLOPT_USERPWD, $aParams["userpwd"]); } $res = curl_exec($ch); if (!$res) { $iErrorCode = curl_errno($ch); $sErrorMsg = curl_error($ch); curl_close($ch); return [ RESULT_ERROR, 'ERROR: failed to fetch ' . $aParams["url"] . ' - curl error #' . $iErrorCode . ': ' . $sErrorMsg ]; } $sOut = ''; $bError = false; $aInfos = curl_getinfo($ch); curl_close($ch); $aTmp = explode("\r\n\r\n", $res, 2); $sHttpHeader = $aTmp[0]; $sHttpBody = $aTmp[1] ?? false; $sOut .= "Http status: " . $aInfos['http_code'] . " - "; if (isset($aParams["status"])) { if ($aInfos['http_code'] === $aParams["status"]) { $sOut .= "compare OK<br>"; } else { $sOut .= "compare failed<br>"; $bError = true; } } else { if ($aInfos['http_code'] >= 400) { $sOut .= "Error page detected<br>"; $bError = true; } else { $sOut .= "request successful<br>"; } } if (isset($aParams["headercontains"]) && $aParams["headercontains"]) { $sOut .= "Http header contains "" . $aParams["headercontains"] . "" - "; if (!strstr($sHttpHeader, $aParams["headercontains"]) === false) { $sOut .= "compare OK<br>"; } else { $sOut .= "compare failed<br>"; $bError = true; } } if (isset($aParams["headernotcontains"]) && $aParams["headernotcontains"]) { $sOut .= "Http header does not contain "" . $aParams["headernotcontains"] . "" - "; if (strstr($sHttpHeader, $aParams["headernotcontains"]) === false) { $sOut .= "compare OK<br>"; } else { $sOut .= "compare failed<br>"; $bError = true; } } if (isset($aParams["headerregex"]) && $aParams["headerregex"]) { $sOut .= "Http header regex test "" . $aParams["headerregex"] . "" - "; try { $bRegex = preg_match($aParams["headerregex"], $sHttpHeader); if ($bRegex) { $sOut .= "compare OK<br>"; } else { $sOut .= "compare failed<br>"; $bError = true; } } catch (Exception $e) { $sOut .= "Wrong REGEX<br>" . print_r($e, 1) . '<br>'; $bError = true; } } if (isset($aParams["bodycontains"]) && $aParams["bodycontains"]) { $sOut .= "Http body contains "" . $aParams["bodycontains"] . "" - "; if (!strstr($sHttpBody, $aParams["bodycontains"]) === false) { $sOut .= "compare OK<br>"; } else { $sOut .= "compare failed<br>"; $bError = true; } } if (isset($aParams["bodynotcontains"]) && $aParams["bodynotcontains"]) { $sOut .= "Http body does not contain "" . $aParams["bodynotcontains"] . "" - "; if (strstr($sHttpBody, $aParams["bodynotcontains"]) === false) { $sOut .= "compare OK<br>"; } else { $sOut .= "compare failed<br>"; $bError = true; } } if (isset($aParams["bodyregex"]) && $aParams["bodyregex"]) { $sOut .= "Http body regex test "" . $aParams["bodyregex"] . "" - "; try { $bRegex = preg_match($aParams["bodyregex"], $sHttpBody); if ($bRegex) { $sOut .= "compare OK<br>"; } else { $sOut .= "compare failed<br>"; $bError = true; } } catch (Exception $e) { $sOut .= "Wrong REGEX<br>" . print_r($e, 1) . '<br>'; $bError = true; } } if (!$bError) { return [ RESULT_OK, 'OK: http check "' . $aParams["url"] . '".<br>' . $sOut ]; } else { return [ RESULT_ERROR, 'ERROR: http check "' . $aParams["url"] . '".<br>' . $sOut ]; } } } + class checkLoadmeter extends appmonitorcheck { public function getGroup(): string { return 'monitor'; } protected function _getLoad(): float { if (function_exists('sys_getloadavg')) { $load = sys_getloadavg(); return $load[0]; } else { if (class_exists('COM')) { $wmi = new COM('WinMgmts:\\\\.'); $cpus = $wmi->InstancesOf('Win32_Processor'); $load = 0; if (version_compare('4.50.0', PHP_VERSION) == 1) { while ($cpu = $cpus->Next()) { $load += $cpu->LoadPercentage; } } else { foreach ($cpus as $cpu) { $load += $cpu->LoadPercentage; } } return $load; } return false; } } public function run(array $aParams): array { $fLoad = $this->_getLoad(); if ($fLoad === false) { $iResult = RESULT_UNKNOWN; } else { $iResult = RESULT_OK; if (isset($aParams['warning']) && $aParams['warning'] && $fLoad > $aParams['warning']) { $iResult = RESULT_WARNING; } if (isset($aParams['error']) && $aParams['error'] && $fLoad > $aParams['error']) { $iResult = RESULT_ERROR; } } return [ $iResult, ($fLoad === false ? 'load value is not available' : 'current load is: ' . $fLoad), ($fLoad === false ? [] : [ 'type' => 'counter', 'count' => $fLoad, 'visual' => 'line', ] ) ] ; } } + class checkMysqlConnect extends appmonitorcheck { public function getGroup(): string { return 'database'; } public function run(array $aParams): array { $this->_checkArrayKeys($aParams, "server,user,password,db"); $mysqli = mysqli_init(); if (!$mysqli) { return [RESULT_ERROR, 'ERROR: mysqli_init failed.']; } if (!$mysqli->options(MYSQLI_OPT_CONNECT_TIMEOUT, (isset($aParams["timeout"]) && (int) $aParams["timeout"]) ? (int) $aParams["timeout"] : $this->_iTimeoutTcp)) { return [RESULT_ERROR, 'ERROR: setting mysqli_options failed.']; } $db = (isset($aParams["port"]) && $aParams["port"]) ? $mysqli->real_connect($aParams["server"], $aParams["user"], $aParams["password"], $aParams["db"], $aParams["port"]) : $mysqli->real_connect($aParams["server"], $aParams["user"], $aParams["password"], $aParams["db"]) ; if ($db) { $mysqli->close(); return [RESULT_OK, "OK: Mysql database " . $aParams["db"] . " was connected"]; } else { return [ RESULT_ERROR, "ERROR: Mysql database " . $aParams["db"] . " was not connected. Error " . mysqli_connect_errno() . ": " . mysqli_connect_error() ]; } } } + class checkPdoConnect extends appmonitorcheck { public function getGroup(): string { return 'database'; } public function run(array $aParams): array { $this->_checkArrayKeys($aParams, "connect,user,password"); try { $db = new PDO( $aParams['connect'], $aParams['user'], $aParams['password'], [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => (isset($aParams["timeout"]) && (int) $aParams["timeout"]) ? (int) $aParams["timeout"] : $this->_iTimeoutTcp, ] ); $db = null; return [RESULT_OK, "OK: Database was connected with PDO " . $aParams['connect']]; } catch (PDOException $e) { return [RESULT_ERROR, "ERROR: Database was not connected " . $aParams['connect'] . " was not connected. Error " . $e->getMessage()]; } } } + class checkPhpmodules extends appmonitorcheck { public function getGroup(): string { return 'service'; } public function run(array $aParams): array { $sOut = ''; $bHasError = false; $bHasWarning = false; $aAllMods = get_loaded_extensions(false); if (isset($aParams['required']) && count($aParams['required'])) { $sOut .= 'Required: '; foreach ($aParams['required'] as $sMod) { $sOut .= $sMod . '='; if (!array_search($sMod, $aAllMods) === false) { $sOut .= 'OK;'; } else { $bHasError = true; $sOut .= 'MISS;'; } } } if (isset($aParams['optional']) && count($aParams['optional'])) { $sOut .= ($sOut ? '|' : '') . 'Optional: '; foreach ($aParams['optional'] as $sMod) { $sOut .= $sMod . '='; if (!array_search($sMod, $aAllMods) === false) { $sOut .= 'OK;'; } else { $bHasWarning = true; $sOut .= 'MISS;'; } } } if ($bHasError) { return [RESULT_ERROR, "ERROR: " . $sOut]; } if ($bHasWarning) { return [RESULT_WARNING, "WARNING: " . $sOut]; } return [RESULT_OK, "OK: " . $sOut]; } } + class checkPing extends appmonitorcheck { public function getGroup(): string { return 'network'; } public function run(array $aParams): array { $sHost = $aParams['host'] ?? '127.0.0.1'; $sParamCount = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' ? "n" : "c"; $iRepeat = 1; $sCommand = "ping -$sParamCount $iRepeat $sHost 2>&1"; exec($sCommand, $aOut, $iRc); $sOut = implode("\n", $aOut); if ($iRc > 0) { return [RESULT_ERROR, "ERROR: ping to $sHost failed.\n" . $sOut]; } return [RESULT_OK, "OK: ping to $sHost\n" . $sOut]; } } + class checkPortTcp extends appmonitorcheck { public function getGroup(): string { return 'network'; } public function run(array $aParams): array { $this->_checkArrayKeys($aParams, "port"); $sHost = $aParams['host'] ?? '127.0.0.1'; $iPort = (int) $aParams['port']; if (!function_exists('socket_create')) { return [RESULT_UNKNOWN, "UNKNOWN: Unable to perform tcp test. The socket module is not enabled in the php installation."]; } $socket = @socket_create(AF_INET, SOCK_STREAM, SOL_TCP); if ($socket === false) { return [RESULT_UNKNOWN, "ERROR: $sHost:$iPort was not checked. socket_create() failed: " . socket_strerror(socket_last_error())]; } socket_set_option( $socket, SOL_SOCKET, SO_SNDTIMEO, [ "sec" => (isset($aParams["timeout"]) && (int) $aParams["timeout"]) ? (int) $aParams["timeout"] : $this->_iTimeoutTcp, "usec" => 0 ] ); $result = @socket_connect($socket, $sHost, $iPort); if ($result === false) { $aResult = [RESULT_ERROR, "ERROR: $sHost:$iPort failed. " . socket_strerror(socket_last_error($socket))]; socket_close($socket); return $aResult; } else { socket_close($socket); return [RESULT_OK, "OK: $sHost:$iPort was connected."]; } } } + class checkSimple extends appmonitorcheck { public function run(array $aParams): array { $this->_checkArrayKeys($aParams, "result,value"); $aData = []; foreach (['type', 'count', 'visual'] as $sMyKey) { if (isset($aParams[$sMyKey])) { $aData[$sMyKey] = $aParams[$sMyKey]; } } return [ $aParams["result"], $aParams["value"], count($aData) ? $aData : false ]; } } + class checkSqliteConnect extends appmonitorcheck { public function getGroup() { return 'database'; } public function run($aParams): array { $this->_checkArrayKeys($aParams, "db"); if (!file_exists($aParams["db"])) { return [ RESULT_ERROR, "ERROR: Sqlite database file " . $aParams["db"] . " does not exist." ]; } if (!isset($aParams['user'])) { $aParams['user'] = ''; } if (!isset($aParams['password'])) { $aParams['password'] = ''; } try { $o = new PDO( "sqlite:" . $aParams["db"], $aParams['user'], $aParams['password'], [ PDO::ATTR_TIMEOUT => (isset($aParams["timeout"]) && (int) $aParams["timeout"]) ? (int) $aParams["timeout"] : $this->_iTimeoutTcp, ] ); return [ RESULT_OK, "OK: Sqlite database " . $aParams["db"] . " was connected" ]; } catch (Exception $e) { return [ RESULT_ERROR, "ERROR: Sqlite database " . $aParams["db"] . " was not connected. " . $e->getMessage() ]; } } } + if (!defined('RESULT_OK')) { define("RESULT_OK", 0); define("RESULT_UNKNOWN", 1); define("RESULT_WARNING", 2); define("RESULT_ERROR", 3); } class appmonitorcheck { protected float $_iStart = 0; protected array $_aConfig = []; protected array $_aData = []; protected array $_units = ['B', 'KB', 'MB', 'GB', 'TB']; protected int $_iTimeoutTcp = 5; protected string $_sPluginDir = __DIR__ . '/../plugins'; public function __construct() { } protected function _createDefaultMetadata(): bool { $this->_aData = [ "name" => $this->_aConfig["name"], "description" => $this->_aConfig["description"], "group" => isset($this->_aConfig["group"]) ? $this->_aConfig["group"] : false, "parent" => isset($this->_aConfig["parent"]) ? $this->_aConfig["parent"] : false, "result" => RESULT_UNKNOWN, "value" => false, "type" => false, "time" => false, ]; return true; } protected function _setResult(int $iResult): true { $this->_aData["result"] = (int) $iResult; return true; } protected function _setOutput(string $s): bool { $this->_aData["value"] = $s; return true; } protected function _setCounter(array $aParams): bool { if (is_array($aParams) && count($aParams)) { foreach (['type', 'count', 'visual'] as $sMyKey) { if (isset($aParams[$sMyKey])) { $this->_aData[$sMyKey] = $aParams[$sMyKey]; } } } return true; } protected function _setReturn(int $iResult, string $s, array $aCounter = []) { $this->_setResult($iResult); $this->_setOutput($s); $this->_setCounter($aCounter); return true; } protected function _checkArrayKeys($aConfig, $sKeyList) { foreach (explode(",", $sKeyList) as $sKey) { if (!isset($aConfig[$sKey])) { header('HTTP/1.0 503 Service Unavailable'); die('<h1>503 Service Unavailable</h1>' . '<h2>Details</h2>' . __METHOD__ . " - array of check parameters requires the keys [$sKeyList] - but key <code>$sKey</code> was not found in config array." . "<pre>" . print_r($aConfig, true) . '</pre>' ); } if (is_null($aConfig[$sKey])) { header('HTTP/1.0 503 Service Unavailable'); die('<h1>503 Service Unavailable</h1>' . '<h2>Details</h2>' . __METHOD__ . " - key <code>$sKey</code> is empty in config array" . "<pre>" . print_r($aConfig, true) . '</pre>' ); } } return true; } public function makeCheck(array $aConfig): array { $this->_iStart = microtime(true); $this->_checkArrayKeys($aConfig, "name,description,check"); $this->_checkArrayKeys($aConfig["check"], "function"); $this->_aConfig = $aConfig; $this->_createDefaultMetadata(); $sCheck = preg_replace('/[^a-zA-Z0-9]/', '', $this->_aConfig["check"]["function"]); $aParams = $this->_aConfig["check"]["params"] ?? []; $sPluginFile = strtolower($this->_sPluginDir . '/checks/' . $sCheck . '.php'); $sCheckClass = 'check' . $sCheck; if (!class_exists($sCheckClass)) { if (file_exists($sPluginFile)) { } } if (!class_exists($sCheckClass)) { header('HTTP/1.0 503 Service Unavailable'); die('<h1>503 Service Unavailable</h1>' . '<h2>Details</h2>' . __METHOD__ . " - check class not found: <code>$sCheckClass</code>" . "<pre>" . print_r($aConfig, true) . '</pre>' . "<h2>Known checks</h2>\n" . print_r($this->listChecks(), 1) ); } $oPlugin = new $sCheckClass; $aResponse = $oPlugin->run($aParams); if (!is_array($aResponse)) { header('HTTP/1.0 503 Service Unavailable'); die('<h1>503 Service Unavailable</h1>' . '<h2>Details</h2>' . __METHOD__ . " - plugin : $sCheck does not responses an array" . "<pre>INPUT " . print_r($aConfig, true) . '</pre>' . "<pre>RESPONSE " . print_r($aResponse, true) . '</pre>' ); } if (count($aResponse) < 2) { header('HTTP/1.0 503 Service Unavailable'); die('<h1>503 Service Unavailable</h1>' . '<h2>Details</h2>' . __METHOD__ . " - plugin : $sCheck does not responses the minimum of 2 array values" . "<pre>INPUT " . print_r($aConfig, true) . '</pre>' . "<pre>RESPONSE " . print_r($aResponse, true) . '</pre>' ); } if (!isset($aResponse[2]) || !$aResponse[2]) { $aResponse[2] = []; } $this->_setReturn($aResponse[0], $aResponse[1], $aResponse[2]); if (!$this->_aData['group'] && method_exists($oPlugin, "getGroup")) { $this->_aData['group'] = $oPlugin->getGroup($aParams); } $this->_aData['time'] = number_format((microtime(true) - $this->_iStart) * 1000, 3) . 'ms'; return $this->respond(); } public function listChecks(): array { $aReturn = []; $class = new ReflectionClass($this); foreach ($class->getMethods(ReflectionMethod::IS_PROTECTED) as $oReflectionMethod) { if (strpos($oReflectionMethod->name, "check") === 0) { $aReturn[(string) $oReflectionMethod->name] = 1; } } foreach (glob($this->_sPluginDir . '/checks/*.php') as $sPluginFile) { $aReturn[str_replace('.php', '', basename($sPluginFile))] = 1; } ksort($aReturn); return array_keys($aReturn); } public function respond() { return $this->_aData; } protected function _certGetInfos(string $sUrl, bool $bVerifyCert): array { $iTimeout = 10; $aUrldata = parse_url($sUrl); $sHost = isset($aUrldata['host']) ? $aUrldata['host'] : false; $iPort = isset($aUrldata['port']) ? $aUrldata['port'] : ((isset($aUrldata['scheme']) && $aUrldata['scheme'] === 'https') ? 443 : false); $aSsl = ['capture_peer_cert' => true]; if ($bVerifyCert) { $aSsl['verify_peer'] = false; $aSsl['verify_peer_name'] = false; } ; $get = stream_context_create(['ssl' => $aSsl]); if (!$get) { return ['_error' => 'Error: Cannot create stream_context']; } $errno = -1; $errstr = "stream_socket_client failed."; $read = stream_socket_client("ssl://$sHost:$iPort", $errno, $errstr, $iTimeout, STREAM_CLIENT_CONNECT, $get); if (!$read) { return ['_error' => "Error $errno: $errstr; cannot create stream_socket_client with given stream_context to ssl://$sHost:$iPort; you can try to set the flag [verify] to false to check expiration date only."]; } $cert = stream_context_get_params($read); if (!$cert) { return ['_error' => "Error: socket was connected to ssl://$sHost:$iPort - but I cannot read certificate infos with stream_context_get_params "]; } return openssl_x509_parse($cert['options']['ssl']['peer_certificate']); } protected function _getHrSize(int $size): string { $power = $size > 0 ? floor(log($size, 1024)) : 0; return number_format($size / pow(1024, $power), 2, '.', ',') . ' ' . $this->_units[$power]; } protected function _getSize(string $sValue): int { if (is_int($sValue)) { return $sValue; } $power = 0; foreach ($this->_units as $sUnit) { if (preg_match('/^[0-9\.\ ]*' . $sUnit . '/', $sValue)) { $i = preg_replace('/([0-9\.]*).*/', '$1', $sValue); $iReal = $i * pow(1024, $power); return $iReal; } $power++; } header('HTTP/1.0 503 Service Unavailable'); die('<h1>503 Service Unavailable</h1>' . '<h2>Details</h2>' . __METHOD__ . " ERROR in space value parameter - there is no size unit in [$sValue] - allowed size units are " . implode('|', $this->_units) ); } } +if (!class_exists('appmonitorcheck')) { } class appmonitor { protected string $_sVersion = 'php-client-v0.137'; protected int $_iDefaultTtl = 300; protected int $_iMaxResult = -1; protected array $_aMeta = []; protected array $_aChecks = []; protected float $_iStart = 0; public function __construct() { $this->_createDefaultMetadata(); } protected function _createDefaultMetadata(): bool { $this->_iStart = microtime(true); $this->_aMeta = [ "host" => false, "website" => false, "ttl" => false, "result" => false, "time" => false, "version" => $this->_sVersion, ]; $this->setHost(); $this->setWebsite(); $this->setTTL(); return true; } public function setHost(string $s = ''): bool { if (!$s) { $s = php_uname("n"); } if (!$s) { return false; } $this->_aMeta["host"] = $s; return true; } public function setWebsite($sWebsite = ''): bool { if (!$sWebsite && isset($_SERVER["HTTP_HOST"])) { $sWebsite = $_SERVER["HTTP_HOST"]; } if (!$sWebsite) { return false; } $this->_aMeta["website"] = $sWebsite; return true; } public function setTTL($iTTl = 0) { if ($iTTl == 0) { $iTTl = $this->_iDefaultTtl; } return $this->_aMeta["ttl"] = $iTTl; } public function setResult(int $iResult = -1): bool { if ($iResult === -1) { $iResult = $this->_iMaxResult; } $this->_aMeta["result"] = $iResult; return true; } public function addCheck($aJob = []): bool { $oCheck = new appmonitorcheck(); $aCheck = $oCheck->makecheck($aJob); $iMyResult = isset($aJob['worstresult']) ? min($aCheck["result"], $aJob['worstresult']) : $aCheck["result"] ; if (!$this->_iMaxResult || $iMyResult > $this->_iMaxResult) { $this->_iMaxResult = $iMyResult; } $this->_aChecks[] = $aCheck; return true; } protected function _addNotification(string $sType, string $sValue, string $sKey = ''): bool { $sTypeCleaned = preg_replace('/[^a-z]/', '', strtolower($sType)); if (!isset($this->_aMeta['notifications'])) { $this->_aMeta['notifications'] = []; } if (!isset($this->_aMeta['notifications'][$sTypeCleaned])) { $this->_aMeta['notifications'][$sTypeCleaned] = []; } if ($sKey) { $this->_aMeta['notifications'][$sTypeCleaned][$sKey] = $sValue; } else { $this->_aMeta['notifications'][$sTypeCleaned][] = $sValue; } return true; } public function addEmail(string $sEmailAddress) { return $this->_addNotification('email', $sEmailAddress); } public function addSlackWebhook(string $sLabel, string $sSlackWebhookUrl): bool { return $this->_addNotification('slack', $sSlackWebhookUrl, $sLabel); } public function addTag(string $sTag): bool { if (!isset($this->_aMeta['tags'])) { $this->_aMeta['tags'] = []; } $this->_aMeta['tags'][] = str_replace(' ', '_', $sTag); return true; } public function checkIp(array $aAllowedIps = []): bool { if (!isset($_SERVER['REMOTE_ADDR']) || !count($aAllowedIps)) { return true; } $sIP = $_SERVER['REMOTE_ADDR']; foreach ($aAllowedIps as $sIp2Check) { if (strpos($sIP, $sIp2Check) === 0) { return true; } } header('HTTP/1.0 403 Forbidden'); die('ERROR: Your ip address [' . $sIP . '] has no access.'); } public function checkToken(string $sVarname, string $sToken): bool { if (!isset($_GET)) { return true; } if (isset($_GET[$sVarname]) && $_GET[$sVarname] === $sToken) { return true; } header('HTTP/1.0 403 Forbidden'); die('ERROR: A token is required.'); } public function listChecks(): array { $oCheck = new appmonitorcheck(); return $oCheck->listChecks(); } protected function _checkData(): bool { $aErrors = []; if (!count($this->_aChecks)) { $aErrors[] = "No checks have been defined."; } if ($this->_aMeta["result"] === false) { $aErrors[] = "method setResult was not used to set a final result for all checks."; } if (count($aErrors)) { $this->abort( '<h2>Error: client check is not complete</h2><p>Found errors:</p><ol><li>' . implode('<li>', $aErrors) . '</ol><br><br>' ); } return true; } public function abort(string $sMessage): void { header('HTTP/1.0 503 Service Unavailable'); die('<h1>503 Service Unavailable</h1>' . $sMessage); } public function getResults(): array { return [ "meta" => $this->_aMeta, "checks" => $this->_aChecks, ]; } public function render(): string { $this->_checkData(); $this->_aMeta['time'] = number_format((microtime(true) - $this->_iStart) * 1000, 3) . 'ms'; $sOut=json_encode($this->getResults()); header('Content-type: application/json'); header('Cache-Control: cache'); header('max-age: ' . $this->_aMeta["ttl"]); echo $sOut; return $sOut; } public function renderHtmloutput(string $sJson): string { header('Content-type: text/html'); header('Cache-Control: cache'); header('max-age: ' . $this->_aMeta["ttl"]); $aMsg = [ 0 => "OK", 1 => "UNKNOWN", 2 => "WARNING", 3 => "ERROR" ]; $aData = json_decode($sJson, 1); $sOut = ''; $sOut .= '' . '<h2>Metadata</h2>' . '<div class="meta' . (isset($aData['meta']['result']) ? ' result' . $aData['meta']['result'] : '') . '">' . 'Status: ' . (isset($aData['meta']['result']) ? $aMsg[$aData['meta']['result']] : '?') . '<br>' . '</div>' . 'Host: ' . (isset($aData['meta']['host']) ? '<span class="string">' . $aData['meta']['host'] . '</span>' : '?') . '<br>' . 'Website: ' . (isset($aData['meta']['website']) ? '<span class="string">' . $aData['meta']['website'] . '</span>' : '?') . '<br>' . 'Execution time: ' . (isset($aData['meta']['time']) ? '<span class="float">' . $aData['meta']['time'] . '</span>' : '?') . '<br>' . 'Client: ' . (isset($aData['meta']['version']) ? '<span class="string">' . $aData['meta']['version'] . '</span>' : '?') . '<br>' . '<h2>Checks</h2>' ; if (isset($aData['checks'][0]) && count($aData['checks'])) { foreach ($aData['checks'] as $aCheck) { $sOut .= '' . '<span class="result' . $aCheck['result'] . '"> <strong>' . $aCheck['name'] . '</strong></span> <br>' . '<div class="check">' . '<div class="description">' . $aCheck['description'] . '<br>' . $aCheck['value'] . '<br>' . '</div>' . 'Execution time: <span class="float">' . (isset($aCheck['time']) ? $aCheck['time'] : ' - ') . '</span><br>' . 'Group: <span class="string">' . (isset($aCheck['group']) ? $aCheck['group'] : '-') . '</span><br>' . 'parent: <span class="string">' . (isset($aCheck['parent']) ? $aCheck['parent'] : '-') . '</span><br>' . 'Status: ' . $aMsg[$aCheck['result']] . '<br>' . '</div>' ; } } $sOut .= '<h2>List of farbcodes</h2>'; foreach ($aMsg as $i => $sText) { $sOut .= '<span class="result' . $i . '">' . $sText . '</span> '; } $sRaw=json_encode($aData, JSON_PRETTY_PRINT); $sRaw = preg_replace('/:\ \"(.*)\"/U', ': "<span class="string">$1</span>"', $sRaw); $sRaw = preg_replace('/:\ ([0-9]*)/', ': <span class="int">$1</span>', $sRaw); $sRaw = preg_replace('/\"(.*)\":/U', '"<span class="key">$1</span>":', $sRaw); $sOut .= '<h2>Raw result data</h2><pre id="raw">' . $sRaw . '</pre>'; $sOut = '<!DOCTYPE html><html><head>' . '<style>' . 'body{background:#eee; color:#444; font-family: verdana,arial; margin: 0; }' . 'body>div#content{background: #fff; border-radius: 2em; border: 4px solid #abc; box-shadow: 0.5em 0.5em 2em #aaa; margin: 2em 10%; padding: 2em;}' . 'h1{color:#346; margin: 0;}' . 'h2{color:#569; margin-top: 2em;}' . 'pre{background:#f4f4f8; padding: 1em; overflow-x:auto; }' . '#raw .key{color:#808;}' . '#raw .int{color:#3a3; font-weight: bold;}' . '#raw .string{color:#66e;}' . '.check{border: 1px solid #ccc; padding: 0.4em; margin-bottom: 2em;}' . '.description{font-style: italic; padding: 0.4em 1em;}' . '.float{color:#080;}' . '.meta{margin-bottom: 1em;}' . '.result0{background:#aca; border-left: 1em solid #080; padding: 0.5em; }' . '.result1{background:#ccc; border-left: 1em solid #aaa; padding: 0.5em; }' . '.result2{background:#fc9; border-left: 1em solid #860; padding: 0.5em; }' . '.result3{background:#f88; border-left: 1em solid #f00; padding: 0.5em; }' . '.string{color:#338;}' . '</style>' . '<title>' . __CLASS__ . '</title>' . '</head><body>' . '<div id="content">' . '<h1>' . __CLASS__ . ' :: client status</h1>' . $sOut . '</div>' . '</body></html>'; return $sOut; } } \ No newline at end of file diff --git a/public_html/appmonitor/general_include.php b/public_html/appmonitor/general_include.php new file mode 100644 index 0000000000000000000000000000000000000000..a6ffb5eccd18e99a69582845157caf50a8727981 --- /dev/null +++ b/public_html/appmonitor/general_include.php @@ -0,0 +1,32 @@ +<?php +/* ______________________________________________________________________ + * + * A P P M O N I T O R :: CLIENT - CHECK :: GENERAL INCLUDE + * ______________________________________________________________________ + * + * @author: Axel Hahn + * ---------------------------------------------------------------------- + * 2025-01-06 initial version + */ + + +// ---------------------------------------------------------------------- +// set a tag with phase +// ---------------------------------------------------------------------- +$sHost=$_SERVER['HTTP_HOST']; +$sHost2=php_uname("n"); +$sMyPhase='live'; + +foreach (array('dev', 'preview', 'stage', 'demo') as $sPhase){ + if( + strstr($sHost.'.', $sPhase)!==false + || strstr($sHost.'-', $sPhase)!==false + || strstr($sHost2.'.', $sPhase)!==false + || strstr($sHost2.'-', $sPhase)!==false + ) { + $sMyPhase=$sPhase; + break; + } +} +$oMonitor->addTag($sMyPhase); + diff --git a/public_html/appmonitor/git_update_appmonitor.sh b/public_html/appmonitor/git_update_appmonitor.sh new file mode 100755 index 0000000000000000000000000000000000000000..0426661154b1f446ce991eb2b4ad1a33894067d5 --- /dev/null +++ b/public_html/appmonitor/git_update_appmonitor.sh @@ -0,0 +1,272 @@ +#!/bin/bash +# ====================================================================== +# +# +# _____ _____ __ _____ _ _ +# | | | | ___ ___ ___| |___ ___|_| |_ ___ ___ +# |- -| | | | |__ | .'| . | . | | | | . | | | _| . | _| +# |_____|_|_|_|_____| |__,| _| _|_|_|_|___|_|_|_|_| |___|_| +# |_| |_| +# _ _ _ +# ___| |_|___ ___| |_ +# | _| | | -_| | _| +# |___|_|_|___|_|_|_| +# +# +# INSTALLER + UPDATER +# +# This script will install or update the appmonitor client only. +# +# Below the document root of a website create a new directory, +# i.e. [webroot]/appmonitor/ and copy this script there. +# Change the directory "cd [webroot]/appmonitor/" and execute it. +# +# ---------------------------------------------------------------------- +# requires git, rsync +# ---------------------------------------------------------------------- +# 2022-04-11 0.1 <axel.hahn@iml.unibe.ch> first lines +# 2022-04-12 0.2 <axel.hahn@iml.unibe.ch> add help; exclude unneeded files +# 2022-05-03 0.3 <axel.hahn@iml.unibe.ch> create general_include.php +# 2024-07-25 0.4 <axel.hahn@iml.unibe.ch> update quoting and comments +# 2024-07-31 0.5 <axel.hahn@iml.unibe.ch> Show more helpful information; wait on 1st install; added param -n +# 2024-12-23 0.6 <axel.hahn@iml.unibe.ch> remove which command. Maybe it is not installed on a shared hoster. +# 2024-12-26 0.7 <axel.hahn@iml.unibe.ch> rsync test with --version instead of -V (for compatibility with older versions) +# 2025-01-06 0.8 <axel.hahn@iml.unibe.ch> git test with --version instead of -v (for compatibility with older versions) +# ====================================================================== + +# ---------------------------------------------------------------------- +# CONFIG +# ---------------------------------------------------------------------- + +readonly git_repo_url="https://github.com/iml-it/appmonitor.git" +readonly docs_url="https://os-docs.iml.unibe.ch/appmonitor/PHP_client/index.html" +readonly line="______________________________________________________________________________" +readonly version="0.8" + +git_target=/tmp/git_data__appmonitor +client_from="${git_target}/public_html/client" +client_to="." +isUpdate=0 +wait=1 + + +cd "$( dirname "$0" )" || exit 1 + +# ---------------------------------------------------------------------- +# FUNCTIONS +# ---------------------------------------------------------------------- + +# Create a missing file from sample file +# +# global $client_from source dir with git repo data +# global $client_to target dir +# +# param string source file (containing .sample); relative to $client_from +function _fileupdate(){ + local _myfile=$1 + local _newfile=${_myfile//.sample/} + echo -n "Update $client_from/$_myfile --> $client_to/$_newfile ... " + + if [ ! -f "$client_to/$_newfile" ]; then + echo -n "copy ... " + cp "$client_from/$_myfile" "$client_to/$_newfile" || exit 2 + echo "OK" + else + echo "already exists - SKIP " + fi + +} + +# get data from a repo with git clone or git pull +# param string url of public .git repo +# param string local directory where to clone it +function _gitUpdate(){ + local _url=$1 + local _dirgit=$2 + local _rc=0 + if [ -d "$_dirgit" ]; then + cd "$_dirgit" || exit 1 + _logBefore=$( git log -1 ); + echo "Update local data from repo... with git pull " + git pull + _logAfter=$( git log -1 ); + if [ "$_logBefore" != "$_logAfter" ]; then + _rc=1 + fi + cd - >/dev/null || exit 1 + else + echo "Cloning..." + git clone "$_url" "$_dirgit" + _rc=$? + fi + return $_rc +} + + +# ---------------------------------------------------------------------- +# MAIN +# ---------------------------------------------------------------------- + +cat <<ENDOFHEADER +$line + + IML Appmonitor client :: installer + updater v$version +$line + + +ENDOFHEADER + +case "$1" in + -h|--help) + cat <<ENDOFHELP + The IML Appmonitor is free software. + + Source: https://github.com/iml-it/appmonitor + Docs: https://os-docs.iml.unibe.ch/appmonitor + License: GNU GPL 3.0 + + This is a helper script to get the files of the IML Appmonitor + client part only. + + Below the document root of a website create a new directory, + i.e. [webroot]/appmonitor/ and copy this script there. + + This script clones and updates the repository in the /tmp + directory and syncs the client files of it to a given directory. + + In the first run it works like an installer. + On additional runs it updates the files. + + USAGE: + $0 [OPTIONS] [TARGET] + + OPTIONS: + -h|--help + Show this help and exit + -n|--nowait + Do not wait for RETURN on 1st installation. + Use it for an unattended installation. + + PARAMETERS: + TARGET + optional target path for the client files + default target is "." (current directory) + +ENDOFHELP + exit 0 + ;; + -n|--nowait) + wait=0 + ;; + *) + if test -n "$1" + then + if ! test -d "$1" + then + echo "ERROR: target dir [$1] does not exist." + exit 1 + fi + echo "set target to $1" + client_to="$1" + fi +esac + +# which rsync >/dev/null || exit 1 +# which git >/dev/null || exit 1 + +rsync --version >/dev/null || exit 1 +git --version >/dev/null || exit 1 + +test -f general_include.php && isUpdate=1 + +if [ $isUpdate -eq 0 ]; then + cat <<WELCOME + Welcome to the Appmonitor client installation! + + + This is a helper script to get the client files of the IML Appmonitor. + They will be installed into the directory "$client_to" $( test "$client_to" = "." && (echo; echo -n " "; pwd) ) + + If this is not correct, press Ctrl + C to abort and use a + parameter to set another target directory. + + "$( basename "$0" ) -h" shows a help and more options. + + +WELCOME + if [ $wait -eq 1 ]; then + echo -n " RETURN to continue ... " + read -r + fi +else + echo "Updating local files ..." +fi +echo + +echo $line +echo ">>> #1 of 3 >>> update local git data" +echo +echo "URL $git_repo_url" +echo "TO $git_target" +if ! _gitUpdate "$git_repo_url" "$git_target" +then + echo ERROR occured :-/ + exit 1 +fi +echo + + +echo $line +echo ">>> #2 of 3 >>> Sync files of Appmonitor client" +echo +echo "FROM $client_from/*" +echo "TO $client_to" +rsync -rav \ + --exclude "build" \ + --exclude "*.sample.*" \ + --exclude "example.json" \ + --exclude "check-appmonitor-server.php" \ + --exclude "local.php" \ + --exclude "git_update_appmonitor.sh" \ + $client_from/* "$client_to" +echo + +_fileupdate general_include.sample.php + +echo $line +echo ">>> #3 of 3 >>> Diff" +echo +diff --color -r "$client_from" "$client_to" +echo + +if [ $isUpdate -eq 0 ]; then + _fileupdate index.sample.php + cat <<INTRODUCTION +$line + + + DONE! + The Appmonitor client was installed. + + - Please edit index.php and general_include.php. + + - If you have multiple applications below webroot then you can + rename the file index.php to check-[appname].php eg. + check-cms.php, check-blog.php, ... + + - Start "$( basename "$0" )" again to perform an update. + Maybe you want to create a cronjob for this. + +INTRODUCTION +else + echo "Appmonitor client was updated." +fi +echo + +echo "Documentation: $docs_url" +echo +echo $line +echo done. +cp -rp "$client_from/git_update_appmonitor.sh" "$client_to" + +# ---------------------------------------------------------------------- diff --git a/public_html/appmonitor/index.php b/public_html/appmonitor/index.php new file mode 100644 index 0000000000000000000000000000000000000000..2587ce2f0aeb0d08ce56c1989fc83d855f4d2942 --- /dev/null +++ b/public_html/appmonitor/index.php @@ -0,0 +1,159 @@ +<?php +/** + * IML APPMONITOR CHECKS FOR CI SERVER + * + * @author: Axel Hahn + * + * ------------------------------------------------------------------ + * 2025-01-06 initial version + */ + +require_once('classes/appmonitor-client.class.php'); + +$aAppDefaults = [ + "df" => [ + "warning" => "1GB", + "critical" => "100MB" + ] +]; + +$oMonitor = new appmonitor(); + +$sWebroot = dirname(__DIR__); + +include('general_include.php'); + +$sCfgfile="$sWebroot/inc_config.php"; +$aConfig=require_once $sCfgfile; + +$oMonitor->addTag('ci-pkg'); +$oMonitor->addTag('rollout'); + +if(isset($aConfig['monitor-ips']) && is_array($aConfig['monitor-ips']) && count($aConfig['monitor-ips']) > 0) { + $oMonitor->checkIp($aConfig['monitor-ips']); +} + + +// ---------------------------------------------------------------------- +// config file +// ---------------------------------------------------------------------- + +$oMonitor->addCheck( + [ + "name" => "read config file", + "description" => "Check if config file is readable", + "check" => [ + "function" => "File", + "params" => [ + "filename" => $sCfgfile, + "file" => true, + "readable" => true, + ], + ], + ] +); + +// ---------------------------------------------------------------------- +// api and hash data +// ---------------------------------------------------------------------- + +$oMonitor->addCheck( + [ + "name" => "check api dir", + "description" => "Check if api dir is readable and writable", + "parent" => "read config file", + "check" => [ + "function" => "File", + "params" => [ + "filename" => $aConfig['tmpdir'], + "dir" => true, + "readable" => true, + "writable" => true, + ], + ], + ] +); + +if (file_exists($aConfig['tmpdir'] . "/used_hashes.txt")) { + $oMonitor->addCheck( + [ + "name" => "write hashes file", + "description" => "Check if hashes file is writable", + "parent" => "check api dir", + "check" => [ + "function" => "File", + "params" => [ + "filename" => $aConfig['tmpdir'] . "/used_hashes.txt", + "file" => true, + "readable" => true, + "writable" => true, + ], + ], + ] + ); +} + +$oMonitor->addCheck( + [ + "name" => "diskspace api dir", + "description" => "The file storage must have some space left", + "parent" => "check api dir", + "check" => [ + "function" => "Diskfree", + "params" => [ + "directory" => $aConfig['tmpdir'], + "warning" => "5MB", + "critical" => "100KB", + ], + ], + ] +); + +// ---------------------------------------------------------------------- +// packages dir +// ---------------------------------------------------------------------- + +$oMonitor->addCheck( + [ + "name" => "check package dir", + "description" => "Check if package dir is readable", + "parent" => "read config file", + "check" => [ + "function" => "File", + "params" => [ + "filename" => $aConfig['packagedir'], + "dir" => true, + "readable" => true, + ], + ], + ] +); + +$oMonitor->addCheck( + [ + "name" => "diskspace package dir", + "description" => "The file storage must have some space left - warn: " . $aAppDefaults["df"]['warning'] . "/ critical: " . $aAppDefaults["df"]['critical'], + "parent" => "check package dir", + "check" => [ + "function" => "Diskfree", + "params" => [ + "directory" => $aConfig['packagedir'], + "warning" => $aAppDefaults["df"]['warning'], + "critical" => $aAppDefaults["df"]['critical'], + ], + ], + ] +); + +// ---------------------------------------------------------------------- +// system stuff +// ---------------------------------------------------------------------- + +require 'plugins/apps/shared_check_ssl.php'; + +// ---------------------------------------------------------------------- + +$oMonitor->setResult(); +$oMonitor->render(); + +// ---------------------------------------------------------------------- diff --git a/public_html/appmonitor/local.php b/public_html/appmonitor/local.php new file mode 100644 index 0000000000000000000000000000000000000000..8fcc94e1854b48c28d034745c844420c07c989c5 --- /dev/null +++ b/public_html/appmonitor/local.php @@ -0,0 +1,6 @@ +<?php + +require __DIR__ . '/check-appmonitor-server.php'; +$sJson=ob_get_contents(); +ob_end_clean(); +echo $oMonitor->renderHtmloutput($sJson); diff --git a/public_html/appmonitor/plugins/apps/concrete5.php b/public_html/appmonitor/plugins/apps/concrete5.php new file mode 100644 index 0000000000000000000000000000000000000000..fa846205e0702f73170f7b9a0281071bb2578141 --- /dev/null +++ b/public_html/appmonitor/plugins/apps/concrete5.php @@ -0,0 +1,165 @@ +<?php +/* ______________________________________________________________________ + * + * A P P M O N I T O R :: CLIENT - CHECK + * ______________________________________________________________________ + * + * Check for a Concrete5 instance. + * CMS https://www.concrete5.org/ + * + * It checks + * - the write access to the config file + * - the write access to the file storage + * - connect to mysql database (which is read from config) + * - ssl certificate (on https request only) + * + * @author: Axel Hahn - https://www.axel-hahn.de/ + * ---------------------------------------------------------------------- + * 2018-06-30 v1.0 ah + * 2019-05-24 v1.01 ah detect include or standalone mode + * 2024-11-18 v1.02 <axel.hahn@unibe.ch> integrate in appmonitor repository + * 2024-11-22 v1.03 <axel.hahn@unibe.ch> send 400 instead of 503 on error + * 2024-12-21 v1.04 ah short array syntax; add php-modules and parent + * 2025-01-06 v1.05 ah add df + */ + +// ---------------------------------------------------------------------- +// Init +// ---------------------------------------------------------------------- + +$aAppDefaults = [ + "name" => "Concrete5 CMS", + "tags" => ["concrete5", "cms"], + "df" => [ + "warning" => "100MB", + "critical" => "10MB" + ] +]; + +require 'inc_appcheck_start.php'; + +// ---------------------------------------------------------------------- +// Read Concrete5 specific config items +// ---------------------------------------------------------------------- + + +$sConfigfile = $sApproot . '/application/config/database.php'; +if (!file_exists($sConfigfile)) { + header('HTTP/1.0 400 Bad request'); + die('ERROR: Config file was not found. Use ?rel=[subdir] to set the correct subdir to find /application/config/database.php.'); +} + +$aConfig = include($sConfigfile); +$sActive = $aConfig['default-connection']; + +if (!isset($aConfig['connections'][$sActive])) { + header('HTTP/1.0 400 Bad request'); + die('ERROR: Config file application/config/database.php was read - but database connection could not be detected from it in connections -> ' . $sActive . '.'); +} +// print_r($aConfig['connections'][$sActive]); die(); +$aDb = $aConfig['connections'][$sActive]; + +// ---------------------------------------------------------------------- +// checks +// ---------------------------------------------------------------------- + +// required php modules +// see https://documentation.concretecms.org/developers/introduction/system-requirements +$oMonitor->addCheck( + [ + "name" => "PHP modules", + "description" => "Check needed PHP modules", + // "group" => "folder", + "check" => [ + "function" => "Phpmodules", + "params" => [ + "required" => [ + "PDO", + "curl", + "dom", + "fileinfo", + "gd", + "iconv", + "mbstring", + "pdo_mysql", + "xml", + "zip" + ], + "optional" => [], + ], + ], + ] +); + +$oMonitor->addCheck( + [ + "name" => "config file", + "description" => "The config file must be readable and writable", + "check" => [ + "function" => "File", + "params" => [ + "filename" => $sConfigfile, + "file" => true, + "readable" => true, + "writable" => true, + ], + ], + ] +); +$oMonitor->addCheck( + [ + "name" => "check file storage", + "description" => "The file storage must be writable", + "check" => [ + "function" => "File", + "params" => [ + "filename" => "$sApproot/application/files", + "dir" => true, + "writable" => true, + ], + ], + ] +); + +$sPdoConnectString = "mysql:host=$aDb[server];port=3306;dbname=$aDb[database];"; + +$oMonitor->addCheck( + [ + "name" => "Mysql Master", + "description" => "Connect mysql server " . $aDb['server'] . " as user " . $aDb['username'] . " to scheme " . $aDb['database'], + "parent" => "config file", + "check" => [ + "function" => "PdoConnect", + "params" => [ + "connect" => $sPdoConnectString, + "user" => $aDb['username'], + "password" => $aDb['password'], + ], + ], + ] +); + +if (isset($aAppDefaults['df'])) { + + $oMonitor->addCheck( + [ + "name" => "check disk space", + "description" => "The file storage must have some space left - warn: " . $aAppDefaults["df"]['warning'] . "/ critical: " . $aAppDefaults["df"]['critical'], + "parent" => "check file storage", + "check" => [ + "function" => "Diskfree", + "params" => [ + "directory" => "$sApproot/application", + "warning" => $aAppDefaults["df"]['warning'], + "critical" => $aAppDefaults["df"]['critical'], + ], + ], + ] + ); +} + +// ---------------------------------------------------------------------- + +require 'inc_appcheck_end.php'; + +// ---------------------------------------------------------------------- diff --git a/public_html/appmonitor/plugins/apps/dokuwiki.php b/public_html/appmonitor/plugins/apps/dokuwiki.php new file mode 100644 index 0000000000000000000000000000000000000000..c03f3a122b354f9a5f54cb8f563f94eeda27e2ed --- /dev/null +++ b/public_html/appmonitor/plugins/apps/dokuwiki.php @@ -0,0 +1,150 @@ +<?php +/* ______________________________________________________________________ + * + * A P P M O N I T O R :: CLIENT - CHECK + * ______________________________________________________________________ + * + * Check for a Dokuwiki instance. + * https://www.dokuwiki.org/ + * + * @author: Axel Hahn - https://www.axel-hahn.de/ + * ---------------------------------------------------------------------- + * 2024-12-23 v1.00 ah initial version + * 2024-12-26 v1.01 ah fix directory checks + * 2025-01-06 v1.02 ah add df + */ + +// ---------------------------------------------------------------------- +// Init +// ---------------------------------------------------------------------- + +$aAppDefaults = [ + "name" => "Dokuwiki", + "tags" => ["dokuwiki", "wiki"], + "df" => [ + "warning" => "100MB", + "critical" => "10MB" + ] +]; + +require 'inc_appcheck_start.php'; + +// ---------------------------------------------------------------------- +// Read Concrete5 specific config items +// ---------------------------------------------------------------------- + +$sConfigfile = "$sApproot/conf/local.php"; +if (!file_exists($sConfigfile)) { + header('HTTP/1.0 400 Bad request'); + die('ERROR: Config file was not found. Use ?rel=[subdir] to set the correct subdir to find /conf/local.php.'); +} + +// ---------------------------------------------------------------------- +// checks +// ---------------------------------------------------------------------- + +// required php modules +// see https://www.dokuwiki.org/install:php +$oMonitor->addCheck( + [ + "name" => "PHP modules", + "description" => "Check needed PHP modules", + // "group" => "folder", + "check" => [ + "function" => "Phpmodules", + "params" => [ + "required" => [ + "json", + "pcre", + "session", + ], + "optional" => [ + "bz2", + "gd", + "intl", + "mbstring", + "openssl", + "zlib" + ], + ], + ], + ] +); + +$oMonitor->addCheck( + [ + "name" => "config file", + "description" => "The config file must be readable and writable", + "check" => [ + "function" => "File", + "params" => [ + "filename" => $sConfigfile, + "file" => true, + "readable" => true, + "writable" => true, + ], + ], + ] +); + +foreach (['lib/tpl/', 'lib/plugins/',] as $sDir) { + $oMonitor->addCheck( + [ + "name" => "check read dir $sDir", + "description" => "The directory $sDir must be readable", + "group" => "folder", + "check" => [ + "function" => "File", + "params" => [ + "filename" => "$sApproot/$sDir", + "dir" => true, + "readable" => true, + ], + ], + ] + ); +} + + +foreach (['data/attic', 'data/cache', 'data/index', 'data/locks', 'data/log', 'data/media', 'data/meta', 'data/pages', 'data/tmp',] as $sDir) { + $oMonitor->addCheck( + [ + "name" => "check writable dir $sDir", + "description" => "The directory $sDir must be readable and writable", + "group" => "folder", + "check" => [ + "function" => "File", + "params" => [ + "filename" => "$sApproot/$sDir", + "dir" => true, + "readable" => true, + "writable" => true, + ], + ], + ] + ); +} + +if (isset($aAppDefaults['df'])) { + + $oMonitor->addCheck( + [ + "name" => "check disk space", + "description" => "The file storage must have some space left - warn: " . $aAppDefaults["df"]['warning'] . "/ critical: " . $aAppDefaults["df"]['critical'], + "check" => [ + "function" => "Diskfree", + "params" => [ + "directory" => "$sApproot/data", + "warning" => $aAppDefaults["df"]['warning'], + "critical" => $aAppDefaults["df"]['critical'], + ], + ], + ] + ); +} + +// ---------------------------------------------------------------------- + +require 'inc_appcheck_end.php'; + +// ---------------------------------------------------------------------- diff --git a/public_html/appmonitor/plugins/apps/iml-appmonitor-server.php b/public_html/appmonitor/plugins/apps/iml-appmonitor-server.php new file mode 100755 index 0000000000000000000000000000000000000000..87473021d5cbe8b47d9ac9de7b512f732e593636 --- /dev/null +++ b/public_html/appmonitor/plugins/apps/iml-appmonitor-server.php @@ -0,0 +1,273 @@ +<?php +/* ______________________________________________________________________ + * + * A P P M O N I T O R :: CLIENT - CHECKS for server instance + * ______________________________________________________________________ + * + * requires variable $sApproot + * + * @author: Axel Hahn + * ---------------------------------------------------------------------- + * 2019-04-29 aded check for ssl cert; removed a check + * 2019-05-17 aded check http to config- and tmp dir + * 2021-11-nn removed all checks ... created as single files + * 2022-03-28 move checks into plugins/apps/ + * 2024-07-23 php 8: short array syntax + * 2024-12-28 added check for custom config and url file (if they exist) + */ + +// ---------------------------------------------------------------------- +// files and dirs +// ---------------------------------------------------------------------- + +$oMonitor->addCheck( + [ + "name" => "PHP modules", + "description" => "Check needed PHP modules", + // "group" => "folder", + "check" => [ + "function" => "Phpmodules", + "params" => [ + "required" => ["curl"], + "optional" => [], + ], + ], + ] +); + +$oMonitor->addCheck( + [ + "name" => "write to ./tmp/", + "description" => "Check cache storage", + // "group" => "folder", + "check" => [ + "function" => "File", + "params" => [ + "filename" => "$sApproot/server/tmp", + "dir" => true, + "writable" => true, + ], + ], + ] +); +$oMonitor->addCheck( + [ + "name" => "write to ./config/", + "description" => "Check config target directory", + // "group" => "folder", + "check" => [ + "function" => "File", + "params" => [ + "filename" => "$sApproot/server/config", + "dir" => true, + "writable" => true, + ], + ], + ] +); + +$oMonitor->addCheck( + [ + "name" => "check config file", + "description" => "The default config file must be readable", + "parent" => "write to ./config/", + // "group" => "file", + "check" => [ + "function" => "File", + "params" => [ + "filename" => "$sApproot/server/config/appmonitor-server-config-defaults.json", + "file" => true, + "readable" => true, + ], + ], + ] +); + +if (is_file("$sApproot/server/config/appmonitor-server-config.json")) { + $oMonitor->addCheck( + [ + "name" => "check custom config file", + "description" => "The custom config file must be readable and writable", + "parent" => "write to ./config/", + // "group" => "file", + "check" => [ + "function" => "File", + "params" => [ + "filename" => "$sApproot/server/config/appmonitor-server-config.json", + "file" => true, + "readable" => true, + "writable" => true, + ], + ], + ] + ); +} + +if (is_file("$sApproot/server/config/appmonitor-server-urls.json")) { + $oMonitor->addCheck( + [ + "name" => "check url file", + "description" => "The url config file must be readable and writable", + "parent" => "write to ./config/", + // "group" => "file", + "check" => [ + "function" => "File", + "params" => [ + "filename" => "$sApproot/server/config/appmonitor-server-urls.json", + "file" => true, + "readable" => true, + "writable" => true, + ], + ], + ] + ); +} + +// ---------------------------------------------------------------------- +// protect dirs against web access +// specialty: if the test results in an error, the total result switches +// to WARNING -> see worstresult value +// ---------------------------------------------------------------------- +$sBaseUrl = 'http' . (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] ? 's' : '') + . '://' . $_SERVER['SERVER_NAME'] . ':' . $_SERVER['SERVER_PORT'] + . dirname(dirname($_SERVER['REQUEST_URI'])); + +foreach (['server/config', 'server/tmp'] as $sMyDir) { + $oMonitor->addCheck( + [ + "name" => "http to $sMyDir", + "description" => "Check if the $sMyDir directory is not accessible (counts as warning on fail)", + "group" => "deny", + "check" => [ + "function" => "HttpContent", + "params" => [ + "url" => "$sBaseUrl/$sMyDir/readme.md", + "status" => 403, + ], + ], + "worstresult" => RESULT_WARNING + ] + ); +} + +// ---------------------------------------------------------------------- +// count of current projects +// ---------------------------------------------------------------------- +require_once($sApproot . '/server/classes/appmonitor-server.class.php'); +$oServer = new appmonitorserver(); +$iCount = count($oServer->getAppIds()); +$oMonitor->addCheck( + [ + "name" => "appcounter", + "description" => "Monitored apps", + "group" => "monitor", + "parent" => false, + "check" => [ + "function" => "Simple", + "params" => [ + "result" => RESULT_OK, + "value" => "Found monitored web apps: $iCount", + "count" => $iCount, + "visual" => "simple", + ], + ], + ] +); +// ---------------------------------------------------------------------- +// check running service +// ---------------------------------------------------------------------- +require_once($sApproot . '/server/classes/tinyservice.class.php'); +ob_start(); +$oService = new tinyservice("$sApproot/server/service.php", 15, "$sApproot/server/tmp"); +$sIsStopped = $oService->canStart(); +$out = ob_get_contents(); +ob_clean(); +$oMonitor->addCheck( + [ + "name" => "running service", + "description" => "Check if the service is running", + "group" => "service", + "check" => [ + "function" => "Simple", + "params" => [ + "result" => ($sIsStopped ? RESULT_WARNING : RESULT_OK), + "value" => ($sIsStopped + ? "Info: Service is NOT running. Apps are checked interactively only (if the appmonitor web ui is running). | Output: $out" + : "OK, service is running. | Output: $out" + ) + ], + ], + "worstresult" => RESULT_OK + ] +); +// ---------------------------------------------------------------------- +// check certificate if https is used +// ---------------------------------------------------------------------- +include 'shared_check_ssl.php'; + + +$oMonitor->addCheck( + [ + "name" => "plugin Load", + "description" => "current load", + "group" => 'monitor', + "parent" => false, + "check" => [ + "function" => "Loadmeter", + "params" => [ + "warning" => 1.0, + "error" => 3, + ], + ], + "worstresult" => RESULT_OK + ] +); + +// ---------------------------------------------------------------------- +// plugin test +// ---------------------------------------------------------------------- +/* + * + * AS A DEMO: using a custom plugin: + * +$oMonitor->addCheck( + [ + "name" => "plugin test", + "description" => "minimal test of the plugin plugins/checkHello.php", + "check" => [ + "function" => "Hello", + "params" => [] + "message" => "Here I am", + ], + ], + ] +); +$oMonitor->addCheck( + [ + "name" => "plugin Load", + "description" => "check current load", + "check" => [ + "function" => "Loadmeter", + "params" => [ + "warning" => 1.0, + "error" => 3, + ], + ], + "worstresult" => RESULT_OK + ] +); +$oMonitor->addCheck( + [ + "name" => "plugin ApacheProcesses", + "description" => "check count running Apache processes", + "check" => [ + "function" => "ApacheProcesses", + "params" => [ + ], + ], + "worstresult" => RESULT_OK + ] +); +*/ + +// ---------------------------------------------------------------------- diff --git a/public_html/appmonitor/plugins/apps/inc_appcheck_end.php b/public_html/appmonitor/plugins/apps/inc_appcheck_end.php new file mode 100644 index 0000000000000000000000000000000000000000..e939ed6d5c31046af8b140082395c6aec270ad85 --- /dev/null +++ b/public_html/appmonitor/plugins/apps/inc_appcheck_end.php @@ -0,0 +1,35 @@ +<?php +/* ______________________________________________________________________ + * + * A P P M O N I T O R :: CLIENT - INCLUDE FOR APP CHECKS :: ON END + * ______________________________________________________________________ + */ + +include 'shared_check_ssl.php'; + +// $bStandalone was set in inc_appcheck_start.php +// send response if client was not initialized there +if ($bStandalone) { + + if(count($oMonitor->getChecks())==0){ + + $oMonitor->addCheck( + [ + "name" => "Simple", + "description" => "Welcome to a simple app check. This is just a quick winner.", + "check" => [ + "function" => "Simple", + "params" => [ + "result" => RESULT_OK, + "value" => "Create a custom check and add all checks you need to test the ability to run the application", + ], + ], + ] + ); + } + + $oMonitor->setResult(); + $oMonitor->render(); +} + +// ---------------------------------------------------------------------- diff --git a/public_html/appmonitor/plugins/apps/inc_appcheck_start.php b/public_html/appmonitor/plugins/apps/inc_appcheck_start.php new file mode 100644 index 0000000000000000000000000000000000000000..3218fc99bd70bff01a289fbf65af2b793cdf1a45 --- /dev/null +++ b/public_html/appmonitor/plugins/apps/inc_appcheck_start.php @@ -0,0 +1,62 @@ +<?php +/* ______________________________________________________________________ + * + * A P P M O N I T O R :: CLIENT - INCLUDE FOR APP CHECKS :: ON START + * ______________________________________________________________________ + */ + +// ---------------------------------------------------------------------- +// CHECK IF THE APPROOT IS SET +// ---------------------------------------------------------------------- + +// initialize client and set very basic metadata ... if needed +$bStandalone = !(class_exists('appmonitor') && isset($oMonitor)); +if ($bStandalone) { + require_once __DIR__ . '/../../classes/appmonitor-client.class.php'; + $oMonitor = new appmonitor(); + + if (!isset($sApproot) || empty($sApproot)) { + $sApproot = $_SERVER['DOCUMENT_ROOT']; + if (isset($_GET['rel'])) { + $sApproot .= str_replace('..', '__', $_GET['rel']); + if (!is_dir($sApproot)) { + header('HTTP/1.0 400 Bad request'); + die('ERROR: The given rel dir does not exist below webroot.'); + } + } + } + + // --- set values coming from app plugins defaults & GET params + // "name" + // "host" + // "tags" + // "dfw", "dfc" + $aAppDefaults['name'] = (isset($_GET['name']) && $_GET['name']) ? $_GET['name'] : $aAppDefaults['name']; + $aAppDefaults['host'] = $_GET['host'] + ? explode(',', $_GET['host']) + : ($_SERVER['HTTP_HOST'] ?? ''); + $aAppDefaults['tags'] = $_GET['tags'] ? explode(',', $_GET['tags']) : $aAppDefaults['tags']; + + $aAppDefaults['df']['warning'] = (isset($_GET['dfw']) && $_GET['dfw']) ? $_GET['dfw'] : $aAppDefaults['df']['warning'] ?? false; + $aAppDefaults['df']['critical'] = (isset($_GET['dfc']) && $_GET['dfc']) ? $_GET['dfc'] : $aAppDefaults['df']['critical'] ?? false; + if($aAppDefaults['df']['warning'] == false && $aAppDefaults['df']['critical'] == false) { + unset($aAppDefaults['df']); + } + + if ($aAppDefaults['name']) { + $oMonitor->setWebsite($aAppDefaults['name']); + } + if ($aAppDefaults['host']) { + $oMonitor->setHost($aAppDefaults['host']); + } + ; + if (isset($aAppDefaults['tags']) && is_array($aAppDefaults['tags']) && count($aAppDefaults['tags']) > 0) { + foreach ($aAppDefaults['tags'] as $sTag) { + $oMonitor->addTag($sTag); + } + } + + @include __DIR__ . '/../../general_include.php'; +} + +// ---------------------------------------------------------------------- diff --git a/public_html/appmonitor/plugins/apps/matomo.php b/public_html/appmonitor/plugins/apps/matomo.php new file mode 100644 index 0000000000000000000000000000000000000000..3be9f6e55adf6b9da63800a8ced5977ec5f98c69 --- /dev/null +++ b/public_html/appmonitor/plugins/apps/matomo.php @@ -0,0 +1,156 @@ +<?php +/* ______________________________________________________________________ + * + * A P P M O N I T O R :: CLIENT - CHECK + * ______________________________________________________________________ + * + * Check for a Matomo instance. + * Open Analytics platform - https://matomo.org/ + * + * It checks + * - the write access to the config file + * - connect to matomo database (which is read from config) + * - ssl certificate (on https request only) + * + * @author: Axel Hahn - https://www.axel-hahn.de/ + * ---------------------------------------------------------------------- + * 2018-06-30 v1.0 + * 2019-05-24 v1.01 detect include or standalone mode + * 2019-05-24 v1.02 detect include or standalone mode + * 2024-12-20 v1.03 <axel.hahn@unibe.ch> integrate in appmonitor repository + * 2024-12-21 v1.04 ah add php-modules and parent + * 2025-01-06 v1.05 ah add checks for writable dirs; add df + */ + +// ---------------------------------------------------------------------- +// Init +// ---------------------------------------------------------------------- + +$aAppDefaults = [ + "name" => "Matomo web statistics", + "tags" => ["matomo", "statistics"], + "df" => [ + "warning" => "100MB", + "critical" => "10MB" + ] +]; + +require 'inc_appcheck_start.php'; + +// ---------------------------------------------------------------------- +// Read Matomo specific config items +// ---------------------------------------------------------------------- + +$sConfigfile = $sApproot . '/config/config.ini.php'; +if (!file_exists($sConfigfile)) { + header('HTTP/1.0 400 Bad request'); + die('ERROR: Config file was not found. Set a correct $sApproot pointing to Matomo install dir.'); +} +$aConfig = parse_ini_file($sConfigfile, true); + + +// ---------------------------------------------------------------------- +// checks +// ---------------------------------------------------------------------- + +// required php modules +// see https://matomo.org/faq/on-premise/matomo-requirements/ +$oMonitor->addCheck( + [ + "name" => "PHP modules", + "description" => "Check needed PHP modules", + // "group" => "folder", + "check" => [ + "function" => "Phpmodules", + "params" => [ + "required" => [ + "PDO", + "curl", + "gd", + "mbstring", + "pdo_mysql", + "xml", + ], + "optional" => [], + ], + ], + ] +); + +$oMonitor->addCheck( + [ + "name" => "config file", + "description" => "The config file must be readable and writable", + "check" => [ + "function" => "File", + "params" => [ + "filename" => $sConfigfile, + "file" => true, + "writable" => true, + ], + ], + ] +); + +$oMonitor->addCheck( + [ + "name" => "Mysql Connect", + "description" => "Connect mysql server " . $aConfig['database']['host'] . " as user " . $aConfig['database']['username'] . " to scheme " . $aConfig['database']['dbname'], + "parent" => "config file", + "check" => [ + "function" => "MysqlConnect", + "params" => [ + "server" => $aConfig['database']['host'], + "user" => $aConfig['database']['username'], + "password" => $aConfig['database']['password'], + "db" => $aConfig['database']['dbname'], + ], + ], + ] +); + + +// directory list from system check +foreach (['/tmp', '/tmp/assets', '/tmp/cache', '/tmp/climulti', '/tmp/latest', '/tmp/logs', '/tmp/sessions', '/tmp/tcpdf', '/tmp/templates_c'] as $sDir) { + $oMonitor->addCheck( + [ + "name" => "check writable dir $sDir", + "description" => "The directory $sDir must be readable and writable", + "group" => "folder", + "check" => [ + "function" => "File", + "params" => [ + "filename" => "$sApproot/$sDir", + "dir" => true, + "readable" => true, + "writable" => true, + ], + ], + ] + ); +} + + +if (isset($aAppDefaults['df'])) { + + $oMonitor->addCheck( + [ + "name" => "check disk space", + "description" => "The file storage must have some space left - warn: " . $aAppDefaults["df"]['warning'] . "/ critical: " . $aAppDefaults["df"]['critical'], + "check" => [ + "function" => "Diskfree", + "params" => [ + "directory" => $sApproot, + "warning" => $aAppDefaults["df"]['warning'], + "critical" => $aAppDefaults["df"]['critical'], + ], + ], + ] + ); +} + +// ---------------------------------------------------------------------- + +require 'inc_appcheck_end.php'; + +// ---------------------------------------------------------------------- \ No newline at end of file diff --git a/public_html/appmonitor/plugins/apps/nextcloud.php b/public_html/appmonitor/plugins/apps/nextcloud.php new file mode 100644 index 0000000000000000000000000000000000000000..231fbb0ad95a459a0001c96c8d730e8e6b79bc99 --- /dev/null +++ b/public_html/appmonitor/plugins/apps/nextcloud.php @@ -0,0 +1,235 @@ +<?php +/* ______________________________________________________________________ + * + * A P P M O N I T O R :: CLIENT - CHECK + * ______________________________________________________________________ + * + * Check for a Nextcloud instance. + * + * It checks + * - the read + write access to the config file + * - connect to database (which is read from config) + * - the read + write access to data dir + * - free disk space on data dir + * - ssl certificate (on https request only) + * + * @author: Axel Hahn - https://www.axel-hahn.de/ + * ---------------------------------------------------------------------- + * 2025-01-02 v1.0 + */ + +// ---------------------------------------------------------------------- +// Init +// ---------------------------------------------------------------------- + +$aAppDefaults = [ + "name" => "Nextcloud", + "tags" => ["nextcloud", "share"], + "df" => [ + "warning" => "1GB", + "critical" => "100MB" + ] +]; + +require 'inc_appcheck_start.php'; + +// ---------------------------------------------------------------------- +// Read Nextcloud specific config items +// ---------------------------------------------------------------------- + +$sConfigfile = "$sApproot/config/config.php"; +if (!file_exists($sConfigfile)) { + header('HTTP/1.0 400 Bad request'); + die('ERROR: Config file was not found. Use ?rel=/NAME or similiar to set a relative install dir.'); +} + +if (!include "$sConfigfile") { + header('HTTP/1.0 400 Bad request'); + die('ERROR: Unable to read config file.'); +} + +// now $CONFIG is available ... +/* +Array +( + [instanceid] => ocw... + [passwordsalt] => cNs... + [secret] => kFdQXw2w... + [trusted_domains] => Array + ( + [0] => https://www.example.com + ) + + [datadirectory] => /home/httpd/cloud/data + [dbtype] => mysql + [version] => 30.0.4.1 + [overwrite.cli.url] => https://www.example.com/cloud + [dbname] => nextcloud + [dbhost] => 127.0.0.1 + [dbport] => + [dbtableprefix] => oc_ + [mysql.utf8mb4] => 1 + [dbuser] => mydbuser + [dbpassword] => 516px9kcc... + [installed] => 1 + [maintenance] => + [theme] => + [loglevel] => 2 + [mail_smtpmode] => smtp + [mail_sendmailmode] => smtp +) +*/ + +if (!isset($CONFIG) || !is_array($CONFIG)) { + header('HTTP/1.0 400 Bad request'); + die('ERROR: Config file was found but has unexpected format.'); +} + + +// ---------------------------------------------------------------------- +// checks +// ---------------------------------------------------------------------- + +// required php modules +// see https://docs.nextcloud.com/server/latest/admin_manual/installation/system_requirements.html +// doesn't show needed modules +/* +$oMonitor->addCheck( + [ + "name" => "PHP modules", + "description" => "Check needed PHP modules", + // "group" => "folder", + "check" => [ + "function" => "Phpmodules", + "params" => [ + "required" => [ + "PDO", + "curl", + "gd", + "mbstring", + "pdo_mysql", + "xml", + ], + "optional" => [], + ], + ], + ] +); +*/ + +$oMonitor->addCheck( + [ + "name" => "config file", + "description" => "The config file must be readable and writable", + "check" => [ + "function" => "File", + "params" => [ + "filename" => $sConfigfile, + "file" => true, + "readable" => true, + "writable" => true, + ], + ], + ] +); +$oMonitor->addCheck( + [ + "name" => "Version", + "description" => "Nextcloud version", + "parent" => "config file", + "check" => [ + "function" => "Simple", + "params" => [ + "result" => RESULT_OK, + "value" => $CONFIG['version'] ?? "??", + "count" => $CONFIG['version'] ?? "??", + "visual" => "simple" + ], + ], + ] +); +$oMonitor->addCheck( + [ + "name" => "maintenance", + "description" => "Is maintenance mode enabled?", + "parent" => "config file", + "check" => [ + "function" => "Simple", + "params" => [ + "result" => $CONFIG['maintenance'] ? RESULT_ERROR : RESULT_OK, + "value" => $CONFIG['maintenance'] ? "enabled - public access is denied" : "disabled (OK)", + ], + ], + ] +); + +// ---------------------------------------------------------------------- +// database +// ---------------------------------------------------------------------- + +if($CONFIG['dbtype'] == "mysql"){ + $oMonitor->addCheck( + [ + "name" => "Mysql Connect", + // "description" => "Connect mysql server " . $aConfig['database']['host'] . " as user " . $aConfig['database']['username'] . " to scheme " . $aConfig['database']['dbname'], + "description" => "Connect mysql server", + "parent" => "config file", + "check" => [ + "function" => "MysqlConnect", + "params" => [ + "server" => $CONFIG['dbhost'], + "user" => $CONFIG['dbuser'], + "password" => $CONFIG['dbpassword'], + "db" => $CONFIG['dbname'], + "port" => $CONFIG['dbport'], + ], + ], + ] + ); +} + +// ---------------------------------------------------------------------- +// data directory +// ---------------------------------------------------------------------- + +$oMonitor->addCheck( + [ + "name" => "data dir", + "description" => "Data directory must be readable and writable", + "parent" => "config file", + "check" => [ + "function" => "File", + "params" => [ + "filename" => $CONFIG['datadirectory'], + "dir" => true, + "readable" => true, + "writable" => true, + ], + ], + ] +); + +if (isset($aAppDefaults['df'])) { + + $oMonitor->addCheck( + [ + "name" => "check disk space", + "description" => "The file storage must have some space left - warn: " . $aAppDefaults["df"]['warning'] . "/ critical: " . $aAppDefaults["df"]['critical'], + "parent" => "data dir", + "check" => [ + "function" => "Diskfree", + "params" => [ + "directory" => $CONFIG['datadirectory'], + "warning" => $aAppDefaults["df"]['warning'], + "critical" => $aAppDefaults["df"]['critical'], + ], + ], + ] + ); +} + +// ---------------------------------------------------------------------- + +require 'inc_appcheck_end.php'; + +// ---------------------------------------------------------------------- diff --git a/public_html/appmonitor/plugins/apps/shared_check_ssl.php b/public_html/appmonitor/plugins/apps/shared_check_ssl.php new file mode 100644 index 0000000000000000000000000000000000000000..c80a3b8da347af8e1d5eefe20dc2d6017c30e91a --- /dev/null +++ b/public_html/appmonitor/plugins/apps/shared_check_ssl.php @@ -0,0 +1,33 @@ +<?php +/* ______________________________________________________________________ + * + * A P P M O N I T O R :: GENERIC CHECK SSL CERT + * + * If https is enabled on standard port 443 the validity of the + * certificate. + * If the current appplication uses http only this check does nothing. + * + * ______________________________________________________________________ + * + * @author: Axel Hahn + * ---------------------------------------------------------------------- + * 2022-03-28 created + */ + + +// ---------------------------------------------------------------------- +// check certificate - only if https is used +// ---------------------------------------------------------------------- +if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS']) { + $oMonitor->addCheck( + [ + "name" => "Certificate check", + "description" => "Check if SSL cert is valid and does not expire soon", + "check" => [ + "function" => "Cert", + ], + ] + ); +} + +// ---------------------------------------------------------------------- diff --git a/public_html/appmonitor/plugins/apps/wordpress.php b/public_html/appmonitor/plugins/apps/wordpress.php new file mode 100644 index 0000000000000000000000000000000000000000..1ac052034d32a09d01b14fc6dae2e730f79c2160 --- /dev/null +++ b/public_html/appmonitor/plugins/apps/wordpress.php @@ -0,0 +1,160 @@ +<?php +/* ______________________________________________________________________ + * + * WORK IN PROGRESS + * + * A P P M O N I T O R :: CLIENT - CHECK + * ______________________________________________________________________ + * + * Check for a Wordpress instance. + * Blogsoftware https://wordpress.org/ + * + * It checks + * - the write access to the config file + * - connect to mysql database (which is read from config) + * - ssl certificate (on https request only) + * + * @author: <axel.hahn@unibe.ch> + * ---------------------------------------------------------------------- + * 2018-11-07 v0.01 + * 2019-05-24 v0.02 detect include or standalone mode + * 2019-05-24 v0.03 detect include or standalone mode + * 2024-07-31 v0.04 first version for wordpress check in plugins/apps/ + * 2024-11-21 v0.05 use shared_check_sl + * 2024-11-22 v0.07 <axel.hahn@unibe.ch> send 400 instead of 503 on error + * 2024-12-21 v0.08 ah add php-modules and parent + * 2025-01-06 v1.05 ah add df + */ + +// ---------------------------------------------------------------------- +// Init +// ---------------------------------------------------------------------- + +$aAppDefaults = [ + "name" => "Wordpress", + "tags" => ["wordpress", "blog"], + "df" => [ + "warning" => "100MB", + "critical" => "10MB" + ] +]; + +require 'inc_appcheck_start.php'; + +// ---------------------------------------------------------------------- +// Read config items +// ---------------------------------------------------------------------- + +$sConfigfile = $sApproot . '/wp-config.php'; +if (!file_exists($sConfigfile)) { + header('HTTP/1.0 400 Bad request'); + die('ERROR: Config file [wp-config.php] was not found. Set a correct app root pointing to wordpress install dir.'); +} + +require($sConfigfile); +$aDb = [ + 'server' => DB_HOST, + 'username' => DB_USER, + 'password' => DB_PASSWORD, + 'database' => DB_NAME, + // 'port' => ??, +]; + +// ---------------------------------------------------------------------- +// checks +// ---------------------------------------------------------------------- + +// required php modules +// see https://ertano.com/required-php-modules-for-wordpress/ +$oMonitor->addCheck( + [ + "name" => "PHP modules", + "description" => "Check needed PHP modules", + // "group" => "folder", + "check" => [ + "function" => "Phpmodules", + "params" => [ + "required" => [ + // "cmath", + "cli", + "curl", + "date", + "dom", + "fileinfo", + "filter", + "gd", + "gettext", + "hash", + "iconv", + "imagick", + "json", + // "libsodium", + "mysql", + "openssl", + "pcre", + // "opcache", + // "readline", + "xml", + "zip" + ], + "optional" => [], + ], + ], + ] +); + +$oMonitor->addCheck( + [ + "name" => "config file", + "description" => "The config file must be writable", + "check" => [ + "function" => "File", + "params" => [ + "filename" => $sConfigfile, + "file" => true, + "readable" => true, + "writable" => true, + ], + ], + ] +); + +$oMonitor->addCheck( + [ + "name" => "Mysql Connect", + "description" => "Connect mysql server " . $aDb['server'] . " as user " . $aDb['username'] . " to scheme " . $aDb['database'], + "parent" => "config file", + "check" => [ + "function" => "MysqlConnect", + "params" => [ + "server" => $aDb['server'], + "user" => $aDb['username'], + "password" => $aDb['password'], + "db" => $aDb['database'], + // "port" => $aDb['port'], + ], + ], + ] +); + +if (isset($aAppDefaults['df'])) { + + $oMonitor->addCheck( + [ + "name" => "check disk space", + "description" => "The file storage must have some space left - warn: " . $aAppDefaults["df"]['warning'] . "/ critical: " . $aAppDefaults["df"]['critical'], + "check" => [ + "function" => "Diskfree", + "params" => [ + "directory" => "$sApproot", + "warning" => $aAppDefaults["df"]['warning'], + "critical" => $aAppDefaults["df"]['critical'], + ], + ], + ] + ); +} + +require 'inc_appcheck_end.php'; + +// ---------------------------------------------------------------------- diff --git a/public_html/appmonitor/plugins/checks/apacheprocesses.php b/public_html/appmonitor/plugins/checks/apacheprocesses.php new file mode 100755 index 0000000000000000000000000000000000000000..400c880bf9d2724e6adda5caa3df45d156aa12e7 --- /dev/null +++ b/public_html/appmonitor/plugins/checks/apacheprocesses.php @@ -0,0 +1,210 @@ +<?php +/** + * ____________________________________________________________________________ + * + * _____ _____ __ _____ _ _ + * | | | | ___ ___ ___| |___ ___|_| |_ ___ ___ + * |- -| | | | |__ | .'| . | . | | | | . | | | _| . | _| + * |_____|_|_|_|_____| |__,| _| _|_|_|_|___|_|_|_|_| |___|_| + * |_| |_| + * _ _ _ + * ___| |_|___ ___| |_ + * | _| | | -_| | _| + * |___|_|_|___|_|_|_| + * + * ____________________________________________________________________________ + * + * SHOW COUNT OF ACTIVE APACHE PROCESSES + * ____________________________________________________________________________ + * + * PARAMS: + * url {string} optional: override https server-status page; default is http://localhost/server-status + * warning {integer} optional: limit to switch to warning (in percent); default: 50 + * error {integer} optional: limit to switch to error (in percent); default: 75 + * + * USAGE: + * Example with overriding all existing params + * + * $oMonitor->addCheck( + * [ + * "name" => "plugin ApacheProcesses", + * "description" => "check count running Apache processes", + * "check" => [ + * "function" => "ApacheProcesses", + * "params" => [ + * "url" => "https://localhost/status", + * "warning" => 75, + * "error" => 90, + * ], + * ], + * "worstresult" => RESULT_OK + * ] + * ); + * ____________________________________________________________________________ + * + * 2019-06-07 <axel.hahn@iml.unibe.ch> + * 2022-07-06 <axel.hahn@iml.unibe.ch> set group "monitor" + * 2024-07-23 <axel.hahn@unibe.ch> php 8 only: use typed variables + * + */ +class checkApacheProcesses extends appmonitorcheck +{ + + /** + * url of server status + * @var string + */ + protected string $_sServerStatusUrl = 'http://localhost/server-status'; + + /** + * Warning level in percent + * @var float + */ + protected float $_iWarn = 50; + + /** + * Critical level in percent + * @var float + */ + protected float $_iError = 75; + + /** + * Self documentation (as idea) + * @return array + */ + public function explain(): array + { + return [ + 'name' => 'Plugin ApacheProcesses', + 'descriptionm' => 'Check count running Apache processes', + 'parameters' => [ + 'url' => [ + 'type' => 'string', + 'required' => false, + 'decsription' => 'Override https server-status page; default is http://localhost/server-status; Use it if the protocol to localhost is not http, but https or if it requires an authentication', + 'default' => $this->_sServerStatusUrl, + 'example' => '', + ], + 'warning' => [ + 'type' => 'float', + 'required' => false, + 'decsription' => 'Limit to switch to warning (in percent)', + 'default' => $this->_iWarn, + 'example' => 30, + ], + 'error' => [ + 'type' => 'float', + 'required' => false, + 'decsription' => 'Limit to switch to critical (in percent)', + 'default' => $this->_iError, + 'example' => 50, + ], + ], + ]; + } + + /** + * Fetch http server status and return slots, active and waiting processes + * as array i.e. [total] => 256 \n [free] => 247\n [waiting] => 7\n [active] => 2 + * It returns false if the url is not reachable + * It returns an empty array if the server status could not be parsed from http response + * @return array + */ + protected function _getApacheProcesses(): bool|array + { + $sBody = file_get_contents($this->_sServerStatusUrl); + if (!$sBody) { + return false; + } + $sRegexScoreboard = '/<pre>(.*)\<\/pre\>/U'; + $aScore = []; + $sStatusNobr = str_replace("\n", "", $sBody); + + if (preg_match_all($sRegexScoreboard, $sStatusNobr, $aTmpTable)) { + $sScoreString = $aTmpTable[1][0]; + // $aScore['scoreboard']=$sScoreString; + $aScore['total'] = strlen($sScoreString); + $aScore['free'] = substr_count($sScoreString, '.'); + $aScore['waiting'] = substr_count($sScoreString, '_'); + $aScore['active'] = $aScore['total'] - $aScore['free'] - $aScore['waiting']; + } + return $aScore; + } + + /** + * Get default group of this check + * @return string + */ + public function getGroup(): string + { + return 'monitor'; + } + + /** + * Implemented method: run the check + * @param array $aParams parameters + * @return array + */ + public function run(array $aParams): array + { + + // --- (1) verify if array key(s) exist: + // $this->_checkArrayKeys($aParams, "..."); + if (isset($aParams['url']) && $aParams['url']) { + $this->_sServerStatusUrl = $aParams['url']; + } + if (isset($aParams['warning']) && (int) $aParams['warning']) { + $this->_iWarn = (int) $aParams['warning']; + } + if (isset($aParams['error']) && (int) $aParams['error']) { + $this->_iError = (int) $aParams['error']; + } + + // --- (2) do something magic + $aProcesses = $this->_getApacheProcesses(); + $iActive = $aProcesses ? $aProcesses['active'] : false; + + // set result code + if ($iActive === false) { + $iResult = RESULT_UNKNOWN; + } else { + $sComment = ''; + $iTotal = $aProcesses['total']; + $iResult = RESULT_OK; + if (($iActive / $iTotal * 100) > $this->_iWarn) { + $iResult = RESULT_WARNING; + $sComment = "more than warning level $this->_iWarn %"; + } else { + $sComment = "less than warning level $this->_iWarn %"; + } + if (($iActive / $iTotal * 100) > $this->_iError) { + $iResult = RESULT_ERROR; + $sComment = "more than error level $this->_iError %"; + } + } + + // --- (3) response + // see method appmonitorcheck->_setReturn() + // + // {integer} you should use a RESULT_XYZ constant: + // RESULT_OK|RESULT_UNKNOWN|RESULT_WARNING|RESULT_ERROR + // {string} output text + // {array} optional: counter data + // type => {string} "counter" + // count => {float} value + // visual => {string} one of bar|line|simple (+params) + // + return [ + $iResult, + ($iActive === false ? 'Apache httpd server status is not available' : 'apache processes: ' . print_r($aProcesses, 1)) . ' ' . $sComment, + ($iActive === false + ? [] + : [ + 'type' => 'counter', + 'count' => $iActive, + 'visual' => 'line', + ] + ) + ]; + } +} diff --git a/public_html/appmonitor/plugins/checks/cert.php b/public_html/appmonitor/plugins/checks/cert.php new file mode 100755 index 0000000000000000000000000000000000000000..a6705b23df2429cbeca41d00d63add35432eb81a --- /dev/null +++ b/public_html/appmonitor/plugins/checks/cert.php @@ -0,0 +1,122 @@ +<?php +/** + * ____________________________________________________________________________ + * + * _____ _____ __ _____ _ _ + * | | | | ___ ___ ___| |___ ___|_| |_ ___ ___ + * |- -| | | | |__ | .'| . | . | | | | . | | | _| . | _| + * |_____|_|_|_|_____| |__,| _| _|_|_|_|___|_|_|_|_| |___|_| + * |_| |_| + * _ _ _ + * ___| |_|___ ___| |_ + * | _| | | -_| | _| + * |___|_|_|___|_|_|_| + * + * ____________________________________________________________________________ + * + * CUSTOM CHECK + * + * Check expiration of ssl certificate + * ____________________________________________________________________________ + * + * USAGE: + * + * $oMonitor->addCheck( + * [ + * "name" => "SSL cert", + * "description" => "Check SSL certificate of my domain", + * "check" => [ + * "function" => "Cert", + * "params" => [ + * "url" => "https://www.example.com", + * "warning" => "30", + * ], + * ], + * ] + * ); + * ____________________________________________________________________________ + * + * 2021-10-26 <axel.hahn@iml.unibe.ch> + * 2022-05-02 <axel.hahn@iml.unibe.ch> set warning to 21 days (old value was 30); add "critical" param + * 2022-05-03 <axel.hahn@iml.unibe.ch> critical limit is a warning only (because app is still functional) + * 2024-07-23 <axel.hahn@unibe.ch> php 8 only: use typed variables + * + */ +class checkCert extends appmonitorcheck +{ + /** + * Get default group of this check + * @return string + */ + public function getGroup(): string + { + return 'security'; + } + + /** + * Check SSL certificate + * @param array $aParams + * [ + * "url" optional: url to connect check; default: own protocol + server + * "verify" optional: flag for verification of certificate or check for expiration only; default=true (=verification is on) + * "warning" optional: count of days to warn; default=21 (=3 weeks) + * "critical" optional: count of days to raise critical; default=5 + * ] + * @return array + */ + public function run(array $aParams): array + { + $sUrl = $aParams["url"] ?? 'http' . ($_SERVER['HTTPS'] ? 's' : '') . '://' . $_SERVER['SERVER_NAME'] . ':' . $_SERVER['SERVER_PORT']; + $bVerify = isset($aParams["verify"]) ? !!$aParams["verify"] : true; + $iWarn = isset($aParams["warning"]) ? (int) ($aParams["warning"]) : 21; + $iCrtitcal = isset($aParams["critical"]) ? (int) ($aParams["critical"]) : 5; + + $sMessage = "Checked url: $sUrl ... "; + $certinfo = $this->_certGetInfos($sUrl, $bVerify); + if (isset($certinfo['_error'])) { + return [ + RESULT_ERROR, + $certinfo['_error'] . $sMessage + ]; + } + + $sDNS = $certinfo['extensions']['subjectAltName'] ?? false; + $sHost = parse_url($sUrl, PHP_URL_HOST); + if (strstr($sDNS, "DNS:$sHost") === false) { + return [ + RESULT_ERROR, + "Wrong certificate: $sHost is not listed as DNS alias in [$sDNS]. $sMessage" + ]; + } + + $iDaysleft = round(($certinfo['validTo_time_t'] - date('U')) / 60 / 60 / 24); + $sMessage .= 'Issuer: ' . $certinfo['issuer']['O'] + . '; valid from: ' . date("Y-m-d H:i", $certinfo['validFrom_time_t']) + . ' to ' . date("Y-m-d H:i", $certinfo['validTo_time_t']) . ' ' + . ($iDaysleft ? "($iDaysleft days left)" : "expired since " . (-$iDaysleft) . " days.") + ; + if ($iDaysleft <= 0) { + return [ + RESULT_ERROR, + 'Expired! ' . $sMessage + ]; + } + if ($iDaysleft <= $iWarn) { + return [ + RESULT_WARNING, + ($iDaysleft <= $iCrtitcal + ? 'Expires very soon! ' + : 'Expires soon. ' + ) . $sMessage + ]; + } + // echo '<pre>'; + return [ + RESULT_OK, + 'OK. ' + . ($bVerify ? 'Certificate is valid. ' : '(Verification is disabled; Check for expiration only.) ') + . $sMessage + ]; + } + +} diff --git a/public_html/appmonitor/plugins/checks/diskfree.php b/public_html/appmonitor/plugins/checks/diskfree.php new file mode 100755 index 0000000000000000000000000000000000000000..86e8efc8d4f74547b9e1647074167e59cb3af206 --- /dev/null +++ b/public_html/appmonitor/plugins/checks/diskfree.php @@ -0,0 +1,89 @@ +<?php +/** + * ____________________________________________________________________________ + * + * _____ _____ __ _____ _ _ + * | | | | ___ ___ ___| |___ ___|_| |_ ___ ___ + * |- -| | | | |__ | .'| . | . | | | | . | | | _| . | _| + * |_____|_|_|_|_____| |__,| _| _|_|_|_|___|_|_|_|_| |___|_| + * |_| |_| + * _ _ _ + * ___| |_|___ ___| |_ + * | _| | | -_| | _| + * |___|_|_|___|_|_|_| + * + * ____________________________________________________________________________ + * + * CHECK FOR FREE DISKSPACE + * ____________________________________________________________________________ + * + * 2021-10-26 <axel.hahn@iml.unibe.ch> + * 2024-07-23 <axel.hahn@unibe.ch> php 8 only: use typed variables + * 2025-01-02 <www.axel-hahn.de> update output + */ +class checkDiskfree extends appmonitorcheck +{ + /** + * Get default group of this check + * @return string + */ + public function getGroup(): string + { + return 'disk'; + } + + /** + * Check free disk space on a given directory + * @param array $aParams + * [ + * "directory" directory that must exist + * "warning" space for warning (optional) + * "critical" minimal space + * ] + * @return array + */ + public function run(array $aParams): array + { + $this->_checkArrayKeys($aParams, "directory,critical"); + + $sDirectory = $aParams["directory"]; + if (!is_dir($sDirectory)) { + return [ + RESULT_ERROR, + "directory [$sDirectory] does not exist. Maybe it is wrong or is not mounted." + ]; + } + + $iWarn = isset($aParams["warning"]) ? $this->_getSize($aParams["warning"]) : false; + $iCritical = $this->_getSize($aParams["critical"]); + $iSpaceLeft = disk_free_space($sDirectory); + + $sMessage = $this->_getHrSize($iSpaceLeft) . ' left in [' . $sDirectory . '].'; + + if ($iWarn) { + if ($iWarn <= $iCritical) { + header('HTTP/1.0 503 Service Unavailable'); + die("ERROR in a Diskfree check - warning value must be larger than critical.<pre>" . print_r($aParams, true)); + } + if ($iWarn < $iSpaceLeft) { + return [ + RESULT_OK, + "$sMessage Warning level is not reached yet (still " . $this->_getHrSize($iSpaceLeft - $iWarn) . " over warning limit)." + ]; + } + if ($iWarn > $iSpaceLeft && $iCritical < $iSpaceLeft) { + return [ + RESULT_WARNING, + $sMessage . ' Warning level ' . $this->_getHrSize($iWarn) . ' was reached (space is ' . $this->_getHrSize($iWarn - $iSpaceLeft) . ' below warning limit; still ' . $this->_getHrSize($iSpaceLeft - $iCritical) . ' over critical limit).' + ]; + } + } + // check space + if ($iCritical < $iSpaceLeft) { + return [RESULT_OK, $sMessage . ' Minimum is not reached yet (still ' . $this->_getHrSize($iSpaceLeft - $iCritical) . ' over critical limit).']; + } else { + return [RESULT_ERROR, $sMessage]; + } + } + +} diff --git a/public_html/appmonitor/plugins/checks/exec.php b/public_html/appmonitor/plugins/checks/exec.php new file mode 100644 index 0000000000000000000000000000000000000000..6221ca6fed432c724e4339e6b030d7cc73fd3e14 --- /dev/null +++ b/public_html/appmonitor/plugins/checks/exec.php @@ -0,0 +1,130 @@ +<?php +/** + * ____________________________________________________________________________ + * + * _____ _____ __ _____ _ _ + * | | | | ___ ___ ___| |___ ___|_| |_ ___ ___ + * |- -| | | | |__ | .'| . | . | | | | . | | | _| . | _| + * |_____|_|_|_|_____| |__,| _| _|_|_|_|___|_|_|_|_| |___|_| + * |_| |_| + * _ _ _ + * ___| |_|___ ___| |_ + * | _| | | -_| | _| + * |___|_|_|___|_|_|_| + * + * ____________________________________________________________________________ + * + * CUSTOM CHECK BASED ON SHELL COMMANDS + * + * Execute a shell command. + * ____________________________________________________________________________ + * + * 2022-09-19 <axel.hahn@iml.unibe.ch> + * 2024-07-23 <axel.hahn@unibe.ch> php 8 only: use typed variables + * + */ +class checkExec extends appmonitorcheck +{ + /** + * Get default group of this check + * @return string + */ + public function getGroup() + { + return 'service'; + } + + /** + * Check execution of a command + * @param array $aParams + * [ + * "command" {string} command to execute + * "output" {bool} flag: show output; default: true + * + * "exitOK" {array} array of integegers for ok exitcodes + * "exitWarn" {array} array of integegers for exitcodes with warning + * "exitCritical" {array} array of integegers for exitcodes that result in an error + * + * // TODO ... MAYBE + * "searchOK" {string} search string that must be found in output + * "searchWarn" {string} if search string is found check returns with warning + * "searchCritical" {string} if search string is found check returns with critical + * ] + * @return array + */ + public function run(array $aParams): array + { + $this->_checkArrayKeys($aParams, "command"); + $_sCmd = $aParams['command']; + $_bShowOutput = isset($aParams['output']) ? !!$aParams['output'] : true; + + $_aRcOK = isset($aParams['exitOK']) ? $aParams['exitOK'] : []; + $_aRcWarning = isset($aParams['exitWarn']) ? $aParams['exitWarn'] : []; + $_aRcCritical = isset($aParams['exitCritical']) ? $aParams['exitCritical'] : []; + + $_sMode = 'default'; + if (count($_aRcOK) + count($_aRcWarning) + count($_aRcCritical)) { + $_sMode = 'exitcode'; + } + + exec($_sCmd, $aOutput, $iRc); + $_sOut = $_bShowOutput ? '<br>' . implode("<br>", $aOutput) : ''; + + switch ($_sMode) { + // non-zero exitcode is an error + case "default": + if ($iRc) { + return [ + RESULT_ERROR, + 'command failed with exitcode ' . $iRc . ': [' . $_sCmd . ']' . $_sOut + ]; + } else { + return [ + RESULT_OK, + "OK [$_sCmd] $_sOut" + ]; + } + ; + // break; + ; + + // handle given custom exitcodes + case "exitcode": + if (in_array($iRc, $_aRcCritical)) { + return [ + RESULT_ERROR, + "Critical exitcode $iRc detected: [$_sCmd] $_sOut" + ]; + } + if (in_array($iRc, $_aRcWarning)) { + return [ + RESULT_WARNING, + "Warning exitcode $iRc detected: [$_sCmd] $_sOut" + ]; + } + if ($iRc == 0 || in_array($iRc, $_aRcOK)) { + return [ + RESULT_OK, + "OK exitcode $iRc detected: [$_sCmd] $_sOut" + ]; + } + return [ + RESULT_UNKNOWN, + "UNKNOWN - unhandled exitcode $iRc detected: [$_sCmd] $_sOut" + ]; + case "search": + return [ + RESULT_UNKNOWN, + "UNKNOWN method [$_sMode] - is not implemented yet." + ]; + // break; + ; + default: + return [ + RESULT_UNKNOWN, + 'UNKNOWN mode [' . htmlentities($_sMode) . '].' + ]; + } // switch($_sMode) + } + +} diff --git a/public_html/appmonitor/plugins/checks/file.php b/public_html/appmonitor/plugins/checks/file.php new file mode 100755 index 0000000000000000000000000000000000000000..f0aafde19c2330ced2a1d19762913db36e625b74 --- /dev/null +++ b/public_html/appmonitor/plugins/checks/file.php @@ -0,0 +1,103 @@ +<?php +/** + * ____________________________________________________________________________ + * + * _____ _____ __ _____ _ _ + * | | | | ___ ___ ___| |___ ___|_| |_ ___ ___ + * |- -| | | | |__ | .'| . | . | | | | . | | | _| . | _| + * |_____|_|_|_|_____| |__,| _| _|_|_|_|___|_|_|_|_| |___|_| + * |_| |_| + * _ _ _ + * ___| |_|___ ___| |_ + * | _| | | -_| | _| + * |___|_|_|___|_|_|_| + * + * ____________________________________________________________________________ + * + * CUSTOM CHECK FOR FILE OBJECTS + * + * Check files, directories, links if the exist or not, if they are accessible + * or not. + * ____________________________________________________________________________ + * + * 2021-10-26 <axel.hahn@iml.unibe.ch> + * 2024-07-23 <axel.hahn@unibe.ch> php 8 only: use typed variables + */ +class checkFile extends appmonitorcheck +{ + /** + * Get default group of this check + * @param array $aParams - see run() method + * @return string + */ + public function getGroup(array $aParams = []): string + { + $sReturn = 'file'; + if (isset($aParams['dir'])) { + $sReturn = 'folder'; + } + foreach (['exists', 'executable', 'readable', 'writable'] as $sFlag) { + if (isset($aParams[$sFlag]) && !$aParams[$sFlag]) { + $sReturn = 'deny'; + } + } + return $sReturn; + } + + /** + * Check a file + * @param array $aParams + * [ + * "filename" directory that must exist + * "exists" "filename" must exist/ must be absent + * "dir" filetype directory + * "file" filetype file + * "link" filetype symbolic link + * "executable" flag executable + * "readable" flag is readable + * "writable" flag is writable + * ] + * @return array + */ + public function run(array $aParams): array + { + $aOK = []; + $aErrors = []; + $this->_checkArrayKeys($aParams, "filename"); + $sFile = $aParams["filename"]; + + if (isset($aParams['exists'])) { + $sMyflag = 'exists=' . ($aParams['exists'] ? 'yes' : 'no'); + if (file_exists($sFile) && $aParams['exists']) { + $aOK[] = $sMyflag; + } else { + $aErrors[] = $sMyflag; + } + } + foreach (['dir', 'executable', 'file', 'link', 'readable', 'writable'] as $sFiletest) { + if (isset($aParams[$sFiletest])) { + $sTestCmd = 'return is_' . $sFiletest . '("' . $sFile . '");'; + if (eval ($sTestCmd) && $aParams[$sFiletest]) { + $aOK[] = $sFiletest . '=' . ($aParams[$sFiletest] ? 'yes' : 'no'); + } else { + $aErrors[] = $sFiletest . '=' . ($aParams[$sFiletest] ? 'yes' : 'no'); + } + } + } + $sMessage = (count($aOK) ? ' flags OK: ' . implode('|', $aOK) : '') + . ' ' . (count($aErrors) ? ' flags FAILED: ' . implode('|', $aErrors) : '') + ; + if (count($aErrors)) { + return [ + RESULT_ERROR, + "file test [$sFile] $sMessage" + ]; + } else { + return [ + RESULT_OK, + "file test [$sFile] $sMessage" + ]; + } + } + +} diff --git a/public_html/appmonitor/plugins/checks/hello.php b/public_html/appmonitor/plugins/checks/hello.php new file mode 100755 index 0000000000000000000000000000000000000000..e2ba5e60f28924b57535c5f5da9c68ee8fed8354 --- /dev/null +++ b/public_html/appmonitor/plugins/checks/hello.php @@ -0,0 +1,80 @@ +<?php +/** + * ____________________________________________________________________________ + * + * _____ _____ __ _____ _ _ + * | | | | ___ ___ ___| |___ ___|_| |_ ___ ___ + * |- -| | | | |__ | .'| . | . | | | | . | | | _| . | _| + * |_____|_|_|_|_____| |__,| _| _|_|_|_|___|_|_|_|_| |___|_| + * |_| |_| + * _ _ _ + * ___| |_|___ ___| |_ + * | _| | | -_| | _| + * |___|_|_|___|_|_|_| + * + * ____________________________________________________________________________ + * + * EXAMPLE CUSTOM CHECK THAT SENDS A HELLO + * + * A plugin always is loaded in clases/appmonitor-checks.class.php + * Have look there for the used protected classes + * ____________________________________________________________________________ + * + * PARAMS: + * message {string} a custom message to display + * + * USAGE: + * + * $oMonitor->addCheck( + * [ + * "name" => "hello plugin", + * "description" => "test a plugin ... plugins/checkHello.php", + * "check" => [ + * "function" => "Hello", + * "params" => [ + * "message" => "Here I am", + * ], + * ], + * ] + * ); + * ____________________________________________________________________________ + * + * 2019-06-05 <axel.hahn@iml.unibe.ch> + * 2024-07-23 <axel.hahn@unibe.ch> php 8 only: use typed variables + * + */ +class checkHello extends appmonitorcheck +{ + + /** + * Run the check + * @param array $aParams + * @return array + */ + public function run(array $aParams): array + { + + // --- (1) verify if array key(s) exist: + $this->_checkArrayKeys($aParams, "message"); + + + // --- (2) do something magic + + + // --- (3) response + // see method appmonitorcheck->_setReturn() + // + // {integer} you should use a RESULT_XYZ constant: + // RESULT_OK|RESULT_UNKNOWN|RESULT_WARNING|RESULT_ERROR + // {string} output text + // {array} optional: counter data + // type => {string} "counter" + // count => {float} value + // visual => {string} one of bar|line|simple (+params) + // + return [ + RESULT_OK, + 'Hello world! My message is: ' . $aParams['message'] + ]; + } +} diff --git a/public_html/appmonitor/plugins/checks/httpcontent.php b/public_html/appmonitor/plugins/checks/httpcontent.php new file mode 100755 index 0000000000000000000000000000000000000000..3d981c54e7a34dcf29f9473681d7e2a2be8e950c --- /dev/null +++ b/public_html/appmonitor/plugins/checks/httpcontent.php @@ -0,0 +1,251 @@ +<?php +/** + * ____________________________________________________________________________ + * + * _____ _____ __ _____ _ _ + * | | | | ___ ___ ___| |___ ___|_| |_ ___ ___ + * |- -| | | | |__ | .'| . | . | | | | . | | | _| . | _| + * |_____|_|_|_|_____| |__,| _| _|_|_|_|___|_|_|_|_| |___|_| + * |_| |_| + * _ _ _ + * ___| |_|___ ___| |_ + * | _| | | -_| | _| + * |___|_|_|___|_|_|_| + * + * ____________________________________________________________________________ + * + * CHECK RESPONSE OF AN HTTP REQUEST + * ____________________________________________________________________________ + * + * 2021-10-26 <axel.hahn@iml.unibe.ch> + * 2022-12-21 <axel.hahn@unibe.ch> add flag sslverify + * 2023-07-06 <axel.hahn@unibe.ch> add flag userpwd + * 2024-07-23 <axel.hahn@unibe.ch> php 8 only: use typed variables + * 2024-11-22 <axel.hahn@unibe.ch> Return unknown if curl module is not active + */ +class checkHttpContent extends appmonitorcheck +{ + /** + * Get default group of this check + * It is a "service" icon or "deny" for expected failures + * + * @param array $aParams with optional 'status' containing http response code + * @return string + */ + public function getGroup(array $aParams=[]): string + { + $sReturn = 'service'; + if (isset($aParams['status']) && $aParams['status'] > 300 && $aParams['status'] < 500) { + $sReturn = 'deny'; + } + return $sReturn; + } + + /** + * Make http request and test response header + body + * @param array $aParams + * [ + * url string url to fetch + * userpwd string set user and password; syntax: "[username]:[password]" + * timeout integer optional timeout in sec; default: 5 + * headeronly boolean optional flag to fetch http response herader only; default: false = returns header and body + * follow boolean optional flag to follow a location; default: false = do not follow + * sslverify boolean flag: enable/ disable verification of ssl certificate; default: true (verification is on) + * + * status integer test for an expected http status code; if none is given then test fails on status 400 and greater + * + * headercontains string test for a string in the http response header; it returns OK if the text was found + * headernotcontains string test for a string in the http response header; it returns OK if the text was not found + * headerregex string test for a regex in the http response header; it returns OK if the regex matches; example: "headerregex"=>"/lowercasematch/i" + * + * bodycontains string test for a string in the http response body; it returns OK if the text was found + * bodynotcontains string test for a string in the http response body; it returns OK if the text was not found + * bodyregex string test for a regex in the http response body; it returns OK if the regex matches; example: "headerregex"=>"/lowercasematch/i" + * ] + */ + public function run(array $aParams) + { + $this->_checkArrayKeys($aParams, "url"); + if (!function_exists("curl_init")) { + return [RESULT_UNKNOWN, "UNKNOWN: Unable to perform mysqli test. The php-curl module is not active."]; + } + $bShowContent = (isset($aParams["content"]) && $aParams["content"]) ? true : false; + $ch = curl_init($aParams["url"]); + + curl_setopt($ch, CURLOPT_HEADER, 1); + curl_setopt($ch, CURLOPT_NOBODY, isset($aParams["headeronly"]) && $aParams["headeronly"]); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, isset($aParams["follow"]) && $aParams["follow"]); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, isset($aParams["sslverify"]) ? !!$aParams["sslverify"] : 1); + curl_setopt($ch, CURLOPT_TIMEOUT, (isset($aParams["timeout"]) && (int) $aParams["timeout"]) ? (int) $aParams["timeout"] : $this->_iTimeoutTcp); + if (isset($aParams["userpwd"])) { + curl_setopt($ch, CURLOPT_USERPWD, $aParams["userpwd"]); + } + + $res = curl_exec($ch); + + if (!$res) { + $iErrorCode = curl_errno($ch); + $sErrorMsg = curl_error($ch); + curl_close($ch); + return [ + RESULT_ERROR, + 'ERROR: failed to fetch ' . $aParams["url"] . ' - curl error #' . $iErrorCode . ': ' . $sErrorMsg + ]; + } + $sOut = ''; + $bError = false; + + $aInfos = curl_getinfo($ch); + /* + Array + ( + [url] => https://www.iml.unibe.ch/ + [content_type] => text/html; charset=utf-8 + [http_code] => 200 + [header_size] => 926 + [request_size] => 55 + [filetime] => -1 + [ssl_verify_result] => 20 + [redirect_count] => 0 + [total_time] => 1.812 + [namelookup_time] => 0 + [connect_time] => 0 + [pretransfer_time] => 0.015 + [size_upload] => 0 + [size_download] => 94654 + [speed_download] => 52237 + [speed_upload] => 0 + [download_content_length] => -1 + [upload_content_length] => -1 + [starttransfer_time] => 1.812 + [redirect_time] => 0 + [redirect_url] => + [primary_ip] => 130.92.30.80 + [certinfo] => Array + ( + ) + + [primary_port] => 443 + [local_ip] => 10.1.30.49 + [local_port] => 63597 + ) + */ + + curl_close($ch); + + $aTmp = explode("\r\n\r\n", $res, 2); + $sHttpHeader = $aTmp[0]; + $sHttpBody = $aTmp[1] ?? false; + + // ---------- check functions + + // --- http status code + $sOut .= "Http status: " . $aInfos['http_code'] . " - "; + if (isset($aParams["status"])) { + if ($aInfos['http_code'] === $aParams["status"]) { + $sOut .= "compare OK<br>"; + } else { + $sOut .= "compare failed<br>"; + $bError = true; + } + } else { + if ($aInfos['http_code'] >= 400) { + $sOut .= "Error page detected<br>"; + $bError = true; + } else { + $sOut .= "request successful<br>"; + } + } + // --- http header + if (isset($aParams["headercontains"]) && $aParams["headercontains"]) { + $sOut .= "Http header contains "" . $aParams["headercontains"] . "" - "; + if (!strstr($sHttpHeader, $aParams["headercontains"]) === false) { + $sOut .= "compare OK<br>"; + } else { + $sOut .= "compare failed<br>"; + $bError = true; + } + } + if (isset($aParams["headernotcontains"]) && $aParams["headernotcontains"]) { + $sOut .= "Http header does not contain "" . $aParams["headernotcontains"] . "" - "; + if (strstr($sHttpHeader, $aParams["headernotcontains"]) === false) { + $sOut .= "compare OK<br>"; + } else { + $sOut .= "compare failed<br>"; + $bError = true; + } + } + if (isset($aParams["headerregex"]) && $aParams["headerregex"]) { + $sOut .= "Http header regex test "" . $aParams["headerregex"] . "" - "; + try { + $bRegex = preg_match($aParams["headerregex"], $sHttpHeader); + if ($bRegex) { + $sOut .= "compare OK<br>"; + } else { + $sOut .= "compare failed<br>"; + $bError = true; + } + } catch (Exception $e) { + $sOut .= "Wrong REGEX<br>" . print_r($e, 1) . '<br>'; + $bError = true; + } + } + // --- http body + if (isset($aParams["bodycontains"]) && $aParams["bodycontains"]) { + $sOut .= "Http body contains "" . $aParams["bodycontains"] . "" - "; + if (!strstr($sHttpBody, $aParams["bodycontains"]) === false) { + $sOut .= "compare OK<br>"; + } else { + $sOut .= "compare failed<br>"; + $bError = true; + } + } + if (isset($aParams["bodynotcontains"]) && $aParams["bodynotcontains"]) { + $sOut .= "Http body does not contain "" . $aParams["bodynotcontains"] . "" - "; + if (strstr($sHttpBody, $aParams["bodynotcontains"]) === false) { + $sOut .= "compare OK<br>"; + } else { + $sOut .= "compare failed<br>"; + $bError = true; + } + } + if (isset($aParams["bodyregex"]) && $aParams["bodyregex"]) { + $sOut .= "Http body regex test "" . $aParams["bodyregex"] . "" - "; + try { + $bRegex = preg_match($aParams["bodyregex"], $sHttpBody); + if ($bRegex) { + $sOut .= "compare OK<br>"; + } else { + $sOut .= "compare failed<br>"; + $bError = true; + } + } catch (Exception $e) { + $sOut .= "Wrong REGEX<br>" . print_r($e, 1) . '<br>'; + $bError = true; + } + } + + if (!$bError) { + return [ + RESULT_OK, + 'OK: http check "' . $aParams["url"] . '".<br>' . $sOut + ]; + } else { + return [ + RESULT_ERROR, + 'ERROR: http check "' . $aParams["url"] . '".<br>' . $sOut + ]; + } + + /* + echo '<pre>'; + echo $sOut."<hr>"; + echo "<hr>HEADER: ".htmlentities($sHttpHeader)."<hr>"; + print_r($aParams); print_r($aInfos); + // echo htmlentities($sHttpBody); + die(); + */ + } + +} diff --git a/public_html/appmonitor/plugins/checks/loadmeter.php b/public_html/appmonitor/plugins/checks/loadmeter.php new file mode 100755 index 0000000000000000000000000000000000000000..13911af15f71926e01f23cce97154217fe076ccd --- /dev/null +++ b/public_html/appmonitor/plugins/checks/loadmeter.php @@ -0,0 +1,148 @@ +<?php +/** + * ____________________________________________________________________________ + * + * _____ _____ __ _____ _ _ + * | | | | ___ ___ ___| |___ ___|_| |_ ___ ___ + * |- -| | | | |__ | .'| . | . | | | | . | | | _| . | _| + * |_____|_|_|_|_____| |__,| _| _|_|_|_|___|_|_|_|_| |___|_| + * |_| |_| + * _ _ _ + * ___| |_|___ ___| |_ + * | _| | | -_| | _| + * |___|_|_|___|_|_|_| + * + * ____________________________________________________________________________ + * + * SHOW LOAD AS LINE + * + * A plugin always is loaded in clases/appmonitor-checks.class.php + * Have look there for the used protected classes + * ____________________________________________________________________________ + * + * PARAMS: + * warning {float} limit to switch to warning + * error {float} limit to switch to error + * + * USAGE: + * + * $oMonitor->addCheck( + * [ + * "name" => "plugin Load", + * "description" => "check current load", + * "check" => [ + * "function" => "Loadmeter", + * "params" => [ + * "warning" => 1.0, + * "error" => 3, + * ], + * ], + * "worstresult" => RESULT_OK + * ] + * ); + * ____________________________________________________________________________ + * + * 2019-06-06 <axel.hahn@iml.unibe.ch> + * 2024-07-23 <axel.hahn@unibe.ch> php 8 only: use typed variables + * 2024-07-25 <axel.hahn@unibe.ch> float return with 2 digits behind comma + * + */ +class checkLoadmeter extends appmonitorcheck +{ + /** + * Get default group of this check + * @return string + */ + public function getGroup(): string + { + return 'monitor'; + } + + /** + * Detect load of a machine and return a float value + * windows part was taken from https://stackoverflow.com/questions/5588616/how-do-you-calculate-server-load-in-php + * @return float + */ + protected function _getLoad(): float + { + if (function_exists('sys_getloadavg')) { + $load = sys_getloadavg(); + return $load[0]; + } else { + // Only MS Windows has not implemented sys_getloadavg + // try something else + if (class_exists('COM')) { + $wmi = new COM('WinMgmts:\\\\.'); + $cpus = $wmi->InstancesOf('Win32_Processor'); + $load = 0; + if (version_compare('4.50.0', PHP_VERSION) == 1) { + while ($cpu = $cpus->Next()) { + $load += $cpu->LoadPercentage; + } + } else { + foreach ($cpus as $cpu) { + $load += $cpu->LoadPercentage; + } + } + return $load; + } + return false; + } + } + + /** + * Run the check and get load + * @param array $aParams optional array with keys warning,error + * @return array + */ + public function run(array $aParams): array + { + + // --- (1) verify if array key(s) exist: + // $this->_checkArrayKeys($aParams, "..."); + + // --- (2) do something magic + // $fLoad=rand(0, 1.3); + // $fLoad=$this->_getServerLoad(); + $fLoad = $this->_getLoad(); + + // set result code + if ($fLoad === false) { + $iResult = RESULT_UNKNOWN; + } else { + $iResult = RESULT_OK; + if (isset($aParams['warning']) && $aParams['warning'] && $fLoad > $aParams['warning']) { + $iResult = RESULT_WARNING; + } + if (isset($aParams['error']) && $aParams['error'] && $fLoad > $aParams['error']) { + $iResult = RESULT_ERROR; + } + } + + + // --- (3) response + // see method appmonitorcheck->_setReturn() + // + // {integer} you should use a RESULT_XYZ constant: + // RESULT_OK|RESULT_UNKNOWN|RESULT_WARNING|RESULT_ERROR + // {string} output text + // {array} optional: counter data + // type => {string} "counter" + // count => {float} value + // visual => {string} one of bar|line|simple (+params) + // + return [ + $iResult, + ($fLoad === false ? 'load value is not available' : 'current load is: ' . round($fLoad, 2)), + ($fLoad === false + ? [] + : [ + 'type' => 'counter', + 'count' => round($fLoad, 2), + 'visual' => 'line', + ] + ) + ] + ; + } +} diff --git a/public_html/appmonitor/plugins/checks/mysqlconnect.php b/public_html/appmonitor/plugins/checks/mysqlconnect.php new file mode 100755 index 0000000000000000000000000000000000000000..5fe8a611cccb570bdd57475a915b11a9f16c96eb --- /dev/null +++ b/public_html/appmonitor/plugins/checks/mysqlconnect.php @@ -0,0 +1,76 @@ +<?php +/** + * ____________________________________________________________________________ + * + * _____ _____ __ _____ _ _ + * | | | | ___ ___ ___| |___ ___|_| |_ ___ ___ + * |- -| | | | |__ | .'| . | . | | | | . | | | _| . | _| + * |_____|_|_|_|_____| |__,| _| _|_|_|_|___|_|_|_|_| |___|_| + * |_| |_| + * _ _ _ + * ___| |_|___ ___| |_ + * | _| | | -_| | _| + * |___|_|_|___|_|_|_| + * + * ____________________________________________________________________________ + * + * CHECK DATABASE CONNECTION WITH MYSQLI + * ____________________________________________________________________________ + * + * 2021-10-27 <axel.hahn@iml.unibe.ch> + * 2024-07-23 <axel.hahn@unibe.ch> php 8 only: use typed variables + * 2024-11-22 <axel.hahn@unibe.ch> detect installed mysqli function + */ +class checkMysqlConnect extends appmonitorcheck +{ + /** + * Get default group of this check + * @return string + */ + public function getGroup(): string + { + return 'database'; + } + + /** + * Check mysql connection to a database using mysqli realconnect + * @param array $aParams + * [ + * server string database hostname / ip address + * user string db user + * password string password for db user + * db string schema / database name + * port integer optional: port + * timeout integer optional timeout in sec; default: 5 + * ] + * @return array + */ + public function run(array $aParams): array + { + $this->_checkArrayKeys($aParams, "server,user,password,db"); + if (!function_exists("mysqli_init")) { + return [RESULT_UNKNOWN, "UNKNOWN: Unable to perform mysqli test. The php-mysqli module is not active."]; + } + $mysqli = mysqli_init(); + if (!$mysqli) { + return [RESULT_ERROR, 'ERROR: mysqli_init failed.']; + } + if (!$mysqli->options(MYSQLI_OPT_CONNECT_TIMEOUT, (isset($aParams["timeout"]) && (int) $aParams["timeout"]) ? (int) $aParams["timeout"] : $this->_iTimeoutTcp)) { + return [RESULT_ERROR, 'ERROR: setting mysqli_options failed.']; + } + + $db = (isset($aParams["port"]) && $aParams["port"]) + ? $mysqli->real_connect($aParams["server"], $aParams["user"], $aParams["password"], $aParams["db"], $aParams["port"]) + : $mysqli->real_connect($aParams["server"], $aParams["user"], $aParams["password"], $aParams["db"]) + ; + if ($db) { + $mysqli->close(); + return [RESULT_OK, "OK: Mysql database " . $aParams["db"] . " was connected"]; + } else { + return [ + RESULT_ERROR, + "ERROR: Mysql database " . $aParams["db"] . " was not connected. Error " . mysqli_connect_errno() . ": " . mysqli_connect_error() + ]; + } + } +} diff --git a/public_html/appmonitor/plugins/checks/pdoconnect.php b/public_html/appmonitor/plugins/checks/pdoconnect.php new file mode 100755 index 0000000000000000000000000000000000000000..d95b3d55074553181d307f68e59bdcfcaba9ab1f --- /dev/null +++ b/public_html/appmonitor/plugins/checks/pdoconnect.php @@ -0,0 +1,73 @@ +<?php +/** + * ____________________________________________________________________________ + * + * _____ _____ __ _____ _ _ + * | | | | ___ ___ ___| |___ ___|_| |_ ___ ___ + * |- -| | | | |__ | .'| . | . | | | | . | | | _| . | _| + * |_____|_|_|_|_____| |__,| _| _|_|_|_|___|_|_|_|_| |___|_| + * |_| |_| + * _ _ _ + * ___| |_|___ ___| |_ + * | _| | | -_| | _| + * |___|_|_|___|_|_|_| + * + * ____________________________________________________________________________ + * + * CHECK DATABASE CONNECTION USING PDO + * ____________________________________________________________________________ + * + * 2021-10-27 <axel.hahn@iml.unibe.ch> + * 2024-07-23 <axel.hahn@unibe.ch> php 8 only: use typed variables + * + */ +class checkPdoConnect extends appmonitorcheck +{ + /** + * Get default group of this check + * @return string + */ + public function getGroup(): string + { + return 'database'; + } + /** + * Check connection to a database using pdo + * see http://php.net/manual/en/pdo.drivers.php + * + * @param array $aParams + * [ + * connect string connect string + * user string db user + * password string password for db user + * timeout integer optional timeout in sec; default: 5 + * ] + * @return array + */ + public function run(array $aParams): array + { + $this->_checkArrayKeys($aParams, "connect,user,password"); + + try { + $db = new PDO( + $aParams['connect'], + $aParams['user'], + $aParams['password'], + [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + + // timeout + // Not all drivers support this option; mysqli does + PDO::ATTR_TIMEOUT => (isset($aParams["timeout"]) && (int) $aParams["timeout"]) ? (int) $aParams["timeout"] : $this->_iTimeoutTcp, + // mssql + // PDO::SQLSRV_ATTR_QUERY_TIMEOUT => $this->_iTimeoutTcp, + ] + ); + $db = null; + return [RESULT_OK, "OK: Database was connected with PDO " . $aParams['connect']]; + } catch (PDOException $e) { + return [RESULT_ERROR, "ERROR: Database was not connected " . $aParams['connect'] . " was not connected. Error " . $e->getMessage()]; + } + } + +} diff --git a/public_html/appmonitor/plugins/checks/phpmodules.php b/public_html/appmonitor/plugins/checks/phpmodules.php new file mode 100644 index 0000000000000000000000000000000000000000..b4b8170aa02b816070867397ddc884bfa3dc1557 --- /dev/null +++ b/public_html/appmonitor/plugins/checks/phpmodules.php @@ -0,0 +1,91 @@ +<?php +/** + * ____________________________________________________________________________ + * + * _____ _____ __ _____ _ _ + * | | | | ___ ___ ___| |___ ___|_| |_ ___ ___ + * |- -| | | | |__ | .'| . | . | | | | . | | | _| . | _| + * |_____|_|_|_|_____| |__,| _| _|_|_|_|___|_|_|_|_| |___|_| + * |_| |_| + * _ _ _ + * ___| |_|___ ___| |_ + * | _| | | -_| | _| + * |___|_|_|___|_|_|_| + * + * ____________________________________________________________________________ + * + * CHECK IF NEEDED PHP MODULES ARE INSTALLED + * ____________________________________________________________________________ + * + * 2022-05-06 <axel.hahn@iml.unibe.ch> first lines + * 2024-07-23 <axel.hahn@unibe.ch> php 8 only: use typed variables + * + */ +class checkPhpmodules extends appmonitorcheck +{ + /** + * Get default group of this check + * @return string + */ + public function getGroup(): string + { + return 'service'; + } + + /** + * Check if system is listening to a given port + * @param array $aParams + * [ + * required array list of required php modules + * optional array optional: list of optional php modules + * ] + * @return array + */ + public function run(array $aParams): array + { + $sOut = ''; + $bHasError = false; + $bHasWarning = false; + // $this->_checkArrayKeys($aParams, "required"); + + // --- get all modules + $aAllMods = get_loaded_extensions(false); + + // --- check required modules + if (isset($aParams['required']) && count($aParams['required'])) { + $sOut .= 'Required: '; + foreach ($aParams['required'] as $sMod) { + $sOut .= $sMod . '='; + if (!array_search($sMod, $aAllMods) === false) { + $sOut .= 'OK;'; + } else { + $bHasError = true; + $sOut .= 'MISS;'; + } + } + } + // --- check optional modules + if (isset($aParams['optional']) && count($aParams['optional'])) { + $sOut .= ($sOut ? '|' : '') . 'Optional: '; + foreach ($aParams['optional'] as $sMod) { + $sOut .= $sMod . '='; + if (!array_search($sMod, $aAllMods) === false) { + $sOut .= 'OK;'; + } else { + $bHasWarning = true; + $sOut .= 'MISS;'; + } + } + } + + // --- return result + if ($bHasError) { + return [RESULT_ERROR, "ERROR: " . $sOut]; + } + if ($bHasWarning) { + return [RESULT_WARNING, "WARNING: " . $sOut]; + } + return [RESULT_OK, "OK: " . $sOut]; + } + +} diff --git a/public_html/appmonitor/plugins/checks/ping.php b/public_html/appmonitor/plugins/checks/ping.php new file mode 100644 index 0000000000000000000000000000000000000000..3056314ccbedca581d82c8ae204305046aa37130 --- /dev/null +++ b/public_html/appmonitor/plugins/checks/ping.php @@ -0,0 +1,113 @@ +<?php +/** + * ____________________________________________________________________________ + * + * _____ _____ __ _____ _ _ + * | | | | ___ ___ ___| |___ ___|_| |_ ___ ___ + * |- -| | | | |__ | .'| . | . | | | | . | | | _| . | _| + * |_____|_|_|_|_____| |__,| _| _|_|_|_|___|_|_|_|_| |___|_| + * |_| |_| + * _ _ _ + * ___| |_|___ ___| |_ + * | _| | | -_| | _| + * |___|_|_|___|_|_|_| + * + * ____________________________________________________________________________ + * + * CHECK PING RESPONSE TIME VIA ICMP + * ____________________________________________________________________________ + * + * 2022-07-05 <axel.hahn@iml.unibe.ch> + * 2022-09-16 <axel.hahn@iml.unibe.ch> read error before closing socket. + * 2022-11-22 <axel.hahn@iml.unibe.ch> Use exec with detecting MS Win for the ping parameter for count of pings + * 2024-07-23 <axel.hahn@unibe.ch> php 8 only: use typed variables + */ +class checkPing extends appmonitorcheck +{ + /** + * Get default group of this check + * @return string + */ + public function getGroup(): string + { + return 'network'; + } + + /** + * Check ping to a target + * @param array $aParams + * [ + * host string optional hostname to connect; default: 127.0.0.1 + * timeout integer OBSOLET (because using exec): optional timeout in sec; default: 5 + * ] + * @return array + */ + public function run(array $aParams): array + { + $sHost = $aParams['host'] ?? '127.0.0.1'; + + $sParamCount = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' ? "n" : "c"; + $iRepeat = 1; + + $sCommand = "ping -$sParamCount $iRepeat $sHost 2>&1"; + exec($sCommand, $aOut, $iRc); + $sOut = implode("\n", $aOut); + + if ($iRc > 0) { + return [RESULT_ERROR, "ERROR: ping to $sHost failed.\n" . $sOut]; + } + return [RESULT_OK, "OK: ping to $sHost\n" . $sOut]; + + /* + Socket functions require root :-/ + + if (!function_exists('socket_create')){ + return [RESULT_UNKNOWN, "UNKNOWN: Unable to perform ping test. The socket module is not enabled in the php installation."]; + } + + // ICMP ping packet with a pre-calculated checksum + $package = "\x08\x00\x7d\x4b\x00\x00\x00\x00PingHost"; + $socket = socket_create(AF_INET, SOCK_RAW, getprotobyname('icmp')); + // TODO + if(!$socket){ + die("ERROR: unable to create socket"); + } + socket_set_option( + $socket, + SOL_SOCKET, + SO_RCVTIMEO, + [ + "sec"=>(isset($aParams["timeout"]) && (int)$aParams["timeout"]) ? (int)$aParams["timeout"] : $this->_iTimeoutTcp, // timeout in seconds + "usec"=>0 + ] + ); + + $start = microtime(true); + socket_connect($socket, $sHost, 0); + $connect = socket_send($socket, $package, strLen($package), 0); + if($connect){ + if (socket_read($socket, 255)){ + $result = microtime(true) - $start; + socket_close($socket); + return [RESULT_OK, + "OK: ping to $sHost", + [ + 'type'=>'counter', + 'count'=>$result, + 'visual'=>'line', + ] + + ]; + } else { + $aResult=[RESULT_ERROR, "ERROR: ping to $sHost failed after connect." . socket_strerror(socket_last_error($socket))]; + socket_close($socket); + return $aResult; + } + } else { + return [RESULT_ERROR, "ERROR: ping to $sHost failed. " . socket_strerror(socket_last_error($socket))]; + } + + */ + } + +} diff --git a/public_html/appmonitor/plugins/checks/porttcp.php b/public_html/appmonitor/plugins/checks/porttcp.php new file mode 100755 index 0000000000000000000000000000000000000000..bd47ce5fbdf5919891c239debe68887a6b6b87da --- /dev/null +++ b/public_html/appmonitor/plugins/checks/porttcp.php @@ -0,0 +1,87 @@ +<?php +/** + * ____________________________________________________________________________ + * + * _____ _____ __ _____ _ _ + * | | | | ___ ___ ___| |___ ___|_| |_ ___ ___ + * |- -| | | | |__ | .'| . | . | | | | . | | | _| . | _| + * |_____|_|_|_|_____| |__,| _| _|_|_|_|___|_|_|_|_| |___|_| + * |_| |_| + * _ _ _ + * ___| |_|___ ___| |_ + * | _| | | -_| | _| + * |___|_|_|___|_|_|_| + * + * ____________________________________________________________________________ + * + * CHECK TCP CONNECTION TO A GIVEN PORT + * ____________________________________________________________________________ + * + * 2021-10-27 <axel.hahn@iml.unibe.ch> + * 2022-07-05 <axel.hahn@iml.unibe.ch> send unknown if socket module is not activated. + * 2022-09-16 <axel.hahn@iml.unibe.ch> read error before closing socket. + * 2022-12-05 <axel.hahn@unibe.ch> add @ sign at socket functions to prevent warning + * 2024-07-23 <axel.hahn@unibe.ch> php 8 only: use typed variables + * + */ +class checkPortTcp extends appmonitorcheck +{ + /** + * Get default group of this check + * @return string + */ + public function getGroup(): string + { + return 'network'; + } + + /** + * Check if system is listening to a given port + * @param array $aParams + * [ + * port integer port + * host string optional hostname to connect; default: 127.0.0.1 + * timeout integer optional timeout in sec; default: 5 + * ] + * @return array + */ + public function run(array $aParams): array + { + $this->_checkArrayKeys($aParams, "port"); + + $sHost = $aParams['host'] ?? '127.0.0.1'; + $iPort = (int) $aParams['port']; + + if (!function_exists('socket_create')) { + return [RESULT_UNKNOWN, "UNKNOWN: Unable to perform tcp test. The php-sockets module is not enabled in the php installation."]; + } + + // from http://php.net/manual/de/sockets.examples.php + + $socket = @socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + if ($socket === false) { + return [RESULT_UNKNOWN, "ERROR: $sHost:$iPort was not checked. socket_create() failed: " . socket_strerror(socket_last_error())]; + } + // set socket timeout + socket_set_option( + $socket, + SOL_SOCKET, // socket level + SO_SNDTIMEO, // timeout option + [ + "sec" => (isset($aParams["timeout"]) && (int) $aParams["timeout"]) ? (int) $aParams["timeout"] : $this->_iTimeoutTcp, // timeout in seconds + "usec" => 0 + ] + ); + + $result = @socket_connect($socket, $sHost, $iPort); + if ($result === false) { + $aResult = [RESULT_ERROR, "ERROR: $sHost:$iPort failed. " . socket_strerror(socket_last_error($socket))]; + socket_close($socket); + return $aResult; + } else { + socket_close($socket); + return [RESULT_OK, "OK: $sHost:$iPort was connected."]; + } + } + +} diff --git a/public_html/appmonitor/plugins/checks/simple.php b/public_html/appmonitor/plugins/checks/simple.php new file mode 100755 index 0000000000000000000000000000000000000000..3af69b69008682e7a91ed9cfe2be93ebc27b30ed --- /dev/null +++ b/public_html/appmonitor/plugins/checks/simple.php @@ -0,0 +1,60 @@ +<?php +/** + * ____________________________________________________________________________ + * + * _____ _____ __ _____ _ _ + * | | | | ___ ___ ___| |___ ___|_| |_ ___ ___ + * |- -| | | | |__ | .'| . | . | | | | . | | | _| . | _| + * |_____|_|_|_|_____| |__,| _| _|_|_|_|___|_|_|_|_| |___|_| + * |_| |_| + * _ _ _ + * ___| |_|___ ___| |_ + * | _| | | -_| | _| + * |___|_|_|___|_|_|_| + * + * ____________________________________________________________________________ + * + * SIMPLE CHECK + * ____________________________________________________________________________ + * + * 2021-10-27 <axel.hahn@iml.unibe.ch> + * 2024-07-23 <axel.hahn@unibe.ch> php 8 only: use typed variables + * + */ +class checkSimple extends appmonitorcheck +{ + + /** + * Most simple check: set given values + * Use this function to add a counter + * + * @param array $aParams + * array keys: + * value string description text + * result integer RESULT_nn + * + * brainstorming for a future release + * + * "counter" optioal: array of counter values + * - label string a label + * - value float a number + * - type string one of simple | bar | line + * @return array + */ + public function run(array $aParams): array + { + $this->_checkArrayKeys($aParams, "result,value"); + // $this->_setReturn((int) $aParams["result"], $aParams["value"]); + $aData = []; + foreach (['type', 'count', 'visual'] as $sMyKey) { + if (isset($aParams[$sMyKey])) { + $aData[$sMyKey] = $aParams[$sMyKey]; + } + } + return [ + $aParams["result"], + $aParams["value"], + count($aData) ? $aData : false + ]; + } +} diff --git a/public_html/appmonitor/plugins/checks/sqliteconnect.php b/public_html/appmonitor/plugins/checks/sqliteconnect.php new file mode 100755 index 0000000000000000000000000000000000000000..703db60d7467d1ff6968b659e0d2c3ca8c2d5c22 --- /dev/null +++ b/public_html/appmonitor/plugins/checks/sqliteconnect.php @@ -0,0 +1,82 @@ +<?php +/** + * ____________________________________________________________________________ + * + * _____ _____ __ _____ _ _ + * | | | | ___ ___ ___| |___ ___|_| |_ ___ ___ + * |- -| | | | |__ | .'| . | . | | | | . | | | _| . | _| + * |_____|_|_|_|_____| |__,| _| _|_|_|_|___|_|_|_|_| |___|_| + * |_| |_| + * _ _ _ + * ___| |_|___ ___| |_ + * | _| | | -_| | _| + * |___|_|_|___|_|_|_| + * + * ____________________________________________________________________________ + * + * CHECK SWLITE CONNECTION USING PDO + * ____________________________________________________________________________ + * + * 2021-10-27 <axel.hahn@iml.unibe.ch> + * 2024-07-23 <axel.hahn@unibe.ch> php 8 only: use typed variables + * + */ +class checkSqliteConnect extends appmonitorcheck +{ + /** + * Get default group of this check + * @return string + */ + public function getGroup() + { + return 'database'; + } + + /** + * check sqlite connection + * @param array $aParams + * [ + * db string full path of sqlite file + * timeout integer optional timeout in sec; default: 5 + * ] + * @return array + */ + public function run($aParams): array + { + $this->_checkArrayKeys($aParams, "db"); + if (!file_exists($aParams["db"])) { + return [ + RESULT_ERROR, + "ERROR: Sqlite database file " . $aParams["db"] . " does not exist." + ]; + } + if (!isset($aParams['user'])) { + $aParams['user'] = ''; + } + if (!isset($aParams['password'])) { + $aParams['password'] = ''; + } + try { + // $db = new SQLite3($sqliteDB); + // $db = new PDO("sqlite:".$sqliteDB); + $o = new PDO( + "sqlite:" . $aParams["db"], + $aParams['user'], + $aParams['password'], + [ + PDO::ATTR_TIMEOUT => (isset($aParams["timeout"]) && (int) $aParams["timeout"]) ? (int) $aParams["timeout"] : $this->_iTimeoutTcp, + ] + ); + return [ + RESULT_OK, + "OK: Sqlite database " . $aParams["db"] . " was connected" + ]; + } catch (Exception $e) { + return [ + RESULT_ERROR, + "ERROR: Sqlite database " . $aParams["db"] . " was not connected. " . $e->getMessage() + ]; + } + } + +} diff --git a/public_html/appmonitor/tests/test_plugins.php b/public_html/appmonitor/tests/test_plugins.php new file mode 100644 index 0000000000000000000000000000000000000000..bd62fa4f6eb962052d989d27840f381d2493e6f6 --- /dev/null +++ b/public_html/appmonitor/tests/test_plugins.php @@ -0,0 +1,48 @@ +<?php +/* + TEST CLIENT CHECKS +*/ + +// ---------------------------------------------------------------------- +// INIT +$sApproot = str_replace('\\', '/', dirname(__DIR__)); + + +// require_once(__DIR__.'/../classes/client_all_in_one.php'); +// echo "OK: file client_all_in_one.php was loaded\n"; + +require_once(__DIR__.'/../classes/appmonitor-client.class.php'); + +$oMonitor = new appmonitor(); + +$oMonitor->listChecks(); + + +echo "OK: class appmonitor was initialized\n"; + + +// ---------------------------------------------------------------------- +$oMonitor->addTag('monitoring'); + +// ---------------------------------------------------------------------- +$oMonitor->addCheck( + [ + "name" => "check config subdir", + "description" => "Check config target directory", + "check" => [ + "function" => "File", + "params" => [ + "filename" => $sApproot . "/server/config", + "dir" => true, + "writable" => true, + ], + ], + ] +); +echo "OK: the plugin File check was added.\n"; + +// ---------------------------------------------------------------------- +$oMonitor->setResult(); +echo "OK: setResult() was executed.\n"; + +// ---------------------------------------------------------------------- diff --git a/public_html/inc_config.php.dist b/public_html/inc_config.php.dist index 0d163efdf9224ee8a273cf85b62d4e93e0182d9d..7fca9df2284e01c281bd1c13e0adfcb573812007 100644 --- a/public_html/inc_config.php.dist +++ b/public_html/inc_config.php.dist @@ -29,5 +29,10 @@ return array( 'showdircontent'=>true, // Enable for troubleshooting - 'debug'=>false, + 'debug'=>false, + + // --- monitoring + + // appmonitor ip that is allowed to fetch data + 'monitor-ips'=>[], ); \ No newline at end of file