#!/bin/sh VERSION="23.10.1" show_version() { cat <, Gregory Colpart , Jérémy Lecour , Brice Waegeneire , Mathieu Trossevin and others. evomaintenance comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under certain conditions. See the GNU General Public Licence for details. END } show_help() { cat </dev/null; then mountpoint=$(stat -c '%m' $1) findmnt ${mountpoint} --noheadings --output OPTIONS -O ro else grep /usr /proc/mounts | grep -E '\bro\b' fi } remount_repository_readwrite() { if [ "$(get_system)" = "OpenBSD" ]; then partition=$(stat -f '%Sd' $1) mount -u -w /dev/${partition} 2>/dev/null else mountpoint=$(stat -c '%m' $1) mount -o remount,rw ${mountpoint} syslog "Re-mount ${mountpoint} as read-write to commit in repository $1" fi } remount_repository_readonly() { if [ "$(get_system)" = "OpenBSD" ]; then partition=$(stat -f '%Sd' $1) mount -u -r /dev/${partition} 2>/dev/null else mountpoint=$(stat -c '%m' $1) mount -o remount,ro ${mountpoint} 2>/dev/null syslog "Re-mount ${mountpoint} as read-only after commit to repository $1" fi } hook_commit() { if [ -x "${GIT_BIN}" ]; then # loop on possible directories managed by GIT for dir in ${GIT_REPOSITORIES}; do # tell Git where to find the repository and the work tree (no need to `cd …` there) export GIT_DIR="${dir}/.git" GIT_WORK_TREE="${dir}" # reset variable used to track if a mount point is readonly READONLY_ORIG=0 # If the repository and the work tree exist, try to commit changes if [ -d "${GIT_DIR}" ] && [ -d "${GIT_WORK_TREE}" ]; then CHANGED_LINES=$(${GIT_BIN} status --porcelain | wc -l | tr -d ' ') if [ "${CHANGED_LINES}" != "0" ]; then if [ "${DRY_RUN}" = "1" ]; then # STATS_SHORT=$(${GIT_BIN} diff --stat | tail -1) STATS=$(${GIT_BIN} diff --stat | tail -n ${GIT_STATUS_MAX_LINES}) # GIT_COMMITS_SHORT=$(printf "%s\n%s : %s" "${GIT_COMMITS_SHORT}" "${GIT_DIR}" "${STATS_SHORT}" | sed -e '/^$/d') GIT_COMMITS=$(printf "%s\n%s\n%s" "${GIT_COMMITS}" "${GIT_DIR}" "${STATS}" | sed -e '/^$/d') else # remount mount point read-write if currently readonly is_repository_readonly ${dir} && { READONLY_ORIG=1; remount_repository_readwrite ${dir}; } # commit changes ${GIT_BIN} add --all ${GIT_BIN} commit --message "${MESSAGE}" --author="${USER} <${USER}@evolix.net>" --quiet # remount mount point read-only if it was before test "$READONLY_ORIG" = "1" && remount_repository_readonly ${dir} # Add the SHA to the log file if something has been committed SHA=$(${GIT_BIN} rev-parse --short HEAD) # STATS_SHORT=$(${GIT_BIN} show --stat | tail -1) STATS=$(${GIT_BIN} show --stat --pretty=format:"" | tail -n ${GIT_STATUS_MAX_LINES}) # append commit data, without empty lines # GIT_COMMITS_SHORT=$(printf "%s\n%s : %s –%s" "${GIT_COMMITS_SHORT}" "${GIT_DIR}" "${SHA}" "${STATS_SHORT}" | sed -e '/^$/d') GIT_COMMITS=$(printf "%s\n%s : %s\n%s" "${GIT_COMMITS}" "${GIT_DIR}" "${SHA}" "${STATS}" | sed -e '/^$/d') fi fi fi # unset environment variables to prevent accidental influence on other git commands unset GIT_DIR GIT_WORK_TREE done if [ -n "${GIT_COMMITS}" ]; then # if [ "${VERBOSE}" = "1" ]; then printf "\n********** Commits ****************\n%s\n***********************************\n" "${GIT_COMMITS}" # fi if [ "${DRY_RUN}" != "1" ]; then echo "${GIT_COMMITS}" >> "${LOGFILE}" fi fi fi } hook_db() { SQL_DETAILS=$(echo "${MESSAGE}" | sed "s/'/''/g") PG_QUERY="INSERT INTO evomaint(hostname,userid,ipaddress,begin_date,end_date,details) VALUES ('${HOSTNAME}','${USER}','${IP}','${BEGIN_DATE}',now(),'${SQL_DETAILS}')" if [ "${VERBOSE}" = "1" ]; then printf "\n********** DB query **************\n%s\n***********************************\n" "${PG_QUERY}" fi if [ "${DRY_RUN}" != "1" ] && [ -x "${PSQL_BIN}" ]; then echo "${PG_QUERY}" | ${PSQL_BIN} "${PGDB}" "${PGTABLE}" -h "${PGHOST}" fi } hook_api() { if [ "${VERBOSE}" = "1" ]; then printf "\n********** API call **************\n" printf "curl -f -s -S -X POST [REDACTED] -k -F api_key=[REDACTED] -F action=insertEvoMaintenance -F hostname=%s -F userid=%s -F ipaddress=%s -F begin_date=%s -F end_date='now()' -F details=%s" \ "${HOSTNAME}" "${USER}" "${IP}" "${BEGIN_DATE}" "${MESSAGE}" printf "\n***********************************\n" fi if [ "${DRY_RUN}" != "1" ] && [ -x "${CURL_BIN}" ]; then API_RETURN_STATUS=$(curl -f -s -S -X POST \ "${API_ENDPOINT}" -k \ -F api_key="${API_KEY}" \ -F action=insertEvoMaintenance \ -F hostname="${HOSTNAME}" \ -F userid="${USER}" \ -F ipaddress="${IP}" \ -F begin_date="${BEGIN_DATE}" \ -F end_date='now()' \ -F details="${MESSAGE}") # either cURL or the API backend can throw an error, otherwise it returns this JSON response if [ "$API_RETURN_STATUS" = '{"status":"Ok"}' ]; then echo "API call OK." else echo "API call FAILED." fi fi } format_mail() { cat <> "${LOGFILE}" fi } # load configuration if present. test -f /etc/evomaintenance.cf && . /etc/evomaintenance.cf HOSTNAME=${HOSTNAME:-$(get_fqdn)} EVOMAINTMAIL=${EVOMAINTMAIL:-"evomaintenance-$(echo "${HOSTNAME}" | cut -d- -f1)@${REALM}"} LOGFILE=${LOGFILE:-"/var/log/evomaintenance.log"} HOOK_COMMIT=${HOOK_COMMIT:-"1"} HOOK_DB=${HOOK_DB:-"0"} HOOK_API=${HOOK_API:-"1"} HOOK_MAIL=${HOOK_MAIL:-"1"} DRY_RUN=${DRY_RUN:-"0"} VERBOSE=${VERBOSE:-"0"} AUTO=${AUTO:-"0"} EVOCHECK=${EVOCHECK:-"0"} GIT_STATUS_MAX_LINES=${GIT_STATUS_MAX_LINES:-20} API_ENDPOINT=${API_ENDPOINT:-""} FORCE_USER=${FORCE_USER:-""} # initialize variables MESSAGE="" # GIT_COMMITS_SHORT="" GIT_COMMITS="" # Parse options # based on https://gist.github.com/deshion/10d3cb5f88a21671e17a while :; do case $1 in -h|-\?|--help) show_help exit 0 ;; -V|--version) show_version exit 0 ;; -m|--message) # message options, with value speparated by space if [ -n "$2" ]; then MESSAGE=$2 shift else printf 'ERROR: "--message" requires a non-empty option argument.\n' >&2 exit 1 fi ;; --message=?*) # message options, with value speparated by = MESSAGE=${1#*=} ;; --message=) # message options, without value printf 'ERROR: "--message" requires a non-empty option argument.\n' >&2 exit 1 ;; --no-commit) # disable commit hook HOOK_COMMIT=0 ;; --commit) # enable commit hook HOOK_COMMIT=1 ;; --no-db) # disable DB hook HOOK_DB=0 ;; --db) # enable DB hook HOOK_DB=1 ;; --no-api) # disable API hook HOOK_API=0 ;; --api) # enable API hook HOOK_API=1 ;; --no-mail) # disable mail hook HOOK_MAIL=0 ;; --mail) # enable mail hook HOOK_MAIL=1 ;; --no-auto) # use "manual" mode AUTO=0 ;; --auto) # use "auto" mode AUTO=1 ;; --autosysadmin) # Deprecated, backward compatibility # author change as autosysadmin printf 'WARNING: "--autosysadmin" is deprecated, use "--user autosysadmin".\n' >&2 FORCE_USER="autosysadmin" ;; -u|--user) # user options, with value speparated by space if [ -n "$2" ]; then FORCE_USER=$2 shift else printf 'ERROR: "--user" requires a non-empty option argument.\n' >&2 exit 1 fi ;; --user=?*) # message options, with value speparated by = FORCE_USER=${1#*=} ;; --user=) # message options, without value printf 'ERROR: "--user" requires a non-empty option argument.\n' >&2 exit 1 ;; -n|--dry-run) # disable actual commands DRY_RUN=1 ;; -v|--verbose) # print verbose information VERBOSE=1 ;; --) # End of all options. shift break ;; -?*|[[:alnum:]]*) # ignore unknown options printf 'WARN: Unknown option (ignored): %s\n' "$1" >&2 ;; *) # Default case: If no more options then break out of the loop. break ;; esac shift done # Treat unset variables as an error when substituting. # Only after this line, because some config variables might be missing. set -u # Gather information HOSTNAME_TEXT=$(get_complete_hostname) # TTY=$(get_tty) # WHO=$(get_who) IP=$(get_ip) BEGIN_DATE=$(get_begin_date) END_DATE=$(get_end_date) USER=$(get_user) PATH=${PATH}:/usr/sbin SENDMAIL_BIN=$(command -v sendmail) readonly SENDMAIL_BIN if [ "${HOOK_MAIL}" = "1" ] && [ -z "${SENDMAIL_BIN}" ]; then echo "No \`sendmail' command has been found, can't send mail." 2>&1 fi GIT_BIN=$(command -v git) readonly GIT_BIN if [ "${HOOK_COMMIT}" = "1" ] && [ -z "${GIT_BIN}" ]; then echo "No \`git' command has been found, can't commit changes" 2>&1 fi PSQL_BIN=$(command -v psql) readonly PSQL_BIN if [ "${HOOK_DB}" = "1" ] && [ -z "${PSQL_BIN}" ]; then echo "No \`psql' command has been found, can't save to the database." 2>&1 fi CURL_BIN=$(command -v curl) readonly CURL_BIN if [ "${HOOK_API}" = "1" ] && [ -z "${CURL_BIN}" ]; then echo "No \`curl' command has been found, can't call the API." 2>&1 fi LOGGER_BIN=$(command -v logger) readonly LOGGER_BIN if [ "${HOOK_API}" = "1" ] && [ -z "${API_ENDPOINT}" ]; then echo "No API endpoint specified, can't call the API." 2>&1 fi EVOCHECK_BIN="/usr/share/scripts/evocheck.sh" GIT_REPOSITORIES="/etc /etc/bind /usr/share/scripts" # Add /etc directories from lxc containers if they are git directories if [ -d /var/lib/lxc ]; then GIT_REPOSITORIES="${GIT_REPOSITORIES} $(find -L /var/lib/lxc/ -maxdepth 3 -name 'etc' | tr '\n' ' ' | sed 's/[[:space:]]\+$//')" fi # initialize variable GIT_STATUSES="" # git statuses if [ -x "${GIT_BIN}" ]; then # loop on possible directories managed by GIT for dir in ${GIT_REPOSITORIES}; do RESULT=$(get_repository_status "${dir}") if [ -n "${RESULT}" ]; then # append diff data, without empty lines GIT_STATUSES=$(printf "%s\n%s\n" "${GIT_STATUSES}" "${RESULT}" | sed -e '/^$/d') fi unset RESULT done fi # find out if running in interactive mode, or not if [ -t 0 ]; then INTERACTIVE=1 else INTERACTIVE=0 fi readonly INTERACTIVE if [ "${INTERACTIVE}" = "1" ] && [ "${EVOCHECK}" = "1" ]; then get_evocheck fi if [ -n "${GIT_STATUSES}" ] && [ "${INTERACTIVE}" = "1" ]; then printf "/!\\\ There are some uncommited changes.\n%s\n\n" "${GIT_STATUSES}" fi if [ -z "${MESSAGE}" ]; then if [ "${INTERACTIVE}" = "1" ]; then printf "> Please, enter details about your maintenance:\n" fi read -r MESSAGE fi if [ -z "${MESSAGE}" ]; then echo "no value..." exit 1 fi print_session_data if [ "${INTERACTIVE}" = "1" ] && [ "${AUTO}" = "0" ]; then if [ "${HOOK_COMMIT}" = "1" ] || [ "${HOOK_MAIL}" = "1" ] || [ "${HOOK_DB}" = "1" ]; then printf "\nActions to execute:\n" if [ "${HOOK_COMMIT}" = "1" ]; then printf "* commit changes in repositories\n" fi if [ "${HOOK_MAIL}" = "1" ]; then printf "* send mail to %s\n" "${EVOMAINTMAIL}" fi if [ "${HOOK_DB}" = "1" ]; then printf "* save metadata to the database\n" fi if [ "${HOOK_API}" = "1" ]; then printf "* send metadata to the API\n" fi echo "" answer="" while :; do printf "> Let's continue? [Y,n,i,?] " read -r answer case $answer in [Yy]|"" ) # force "auto" mode, but keep hooks settings AUTO=1 break ;; [Nn] ) # force "auto" mode, and disable all hooks HOOK_COMMIT=0 HOOK_MAIL=0 HOOK_DB=0 HOOK_API=0 AUTO=1 break ;; [Ii] ) # force "manual" mode AUTO=0 break ;; * ) printf "y - yes, execute actions and exit\n" printf "n - no, don't execute actions and exit\n" printf "i - switch to interactive mode\n" printf "? - print this help\n" ;; esac done fi fi if [ "${INTERACTIVE}" = "1" ] && [ "${AUTO}" = "0" ]; then # Commit hook if [ -n "${GIT_STATUSES}" ] && [ "${HOOK_COMMIT}" = "1" ]; then printf "/!\ There are some uncommited changes.\n%s\n\n" "${GIT_STATUSES}" y="Y"; n="n" answer="" while :; do printf "> Do you want to commit the changes? [%s] " "${y},${n}" read -r answer case $answer in [Yy] ) hook_commit; break ;; [Nn] ) break ;; "" ) if [ "${HOOK_COMMIT}" = "1" ]; then hook_commit fi break ;; * ) echo "answer with a valid choice" ;; esac done fi # Mail hook if [ "${HOOK_MAIL}" = "1" ]; then y="Y"; n="n" else y="y"; n="N" fi answer="" while :; do printf "> Do you want to send an email to <%s>? [%s] " "${EVOMAINTMAIL}" "${y},${n},e" read -r answer case $answer in [Yy] ) hook_mail; break ;; [Nn] ) break ;; [Ee] ) printf "> To: [%s] " "${EVOMAINTMAIL}" read -r mail_recipient if [ -n "${mail_recipient}" ]; then EVOMAINTMAIL="${mail_recipient}" fi ;; "" ) if [ "${HOOK_MAIL}" = "1" ]; then hook_mail fi break ;; * ) echo "answer with a valid choice" ;; esac done # Database hook if [ "${HOOK_DB}" = "1" ]; then y="Y"; n="n" else y="y"; n="N" fi answer="" while :; do printf "> Do you want to insert your message into the database? [%s] " "${y},${n}" read -r answer case $answer in [Yy] ) hook_db; break ;; [Nn] ) break ;; "" ) if [ "${HOOK_DB}" = "1" ]; then hook_db fi break ;; * ) echo "answer with a valid choice" ;; esac done # API hook if [ "${HOOK_API}" = "1" ]; then y="Y"; n="n" else y="y"; n="N" fi answer="" while :; do printf "> Do you want to send the metadata to the API? [%s] " "${y},${n}" read -r answer case $answer in [Yy] ) hook_api; break ;; [Nn] ) break ;; "" ) if [ "${HOOK_API}" = "1" ]; then hook_api fi break ;; * ) echo "answer with a valid choice" ;; esac done fi # Log hook hook_log if [ "${INTERACTIVE}" = "0" ] || [ "${AUTO}" = "1" ]; then if [ "${HOOK_COMMIT}" = "1" ]; then hook_commit fi if [ "${HOOK_MAIL}" = "1" ]; then hook_mail fi if [ "${HOOK_DB}" = "1" ]; then hook_db fi if [ "${HOOK_API}" = "1" ]; then hook_api fi fi exit 0