#!/usr/bin/env bash # ====================================================================== # # DEPLOYMENT CLIENT # Roll out a package of IML CI server on a target system. # # ---------------------------------------------------------------------- # 2021-04-19 v0.1 <axel.hahn@iml.unibe.ch> initial version # 2021-05-09 v0.2 <axel.hahn@iml.unibe.ch> chown includes dot files # 2021-05-14 v0.3 <axel.hahn@iml.unibe.ch> add params (list, force, help) # 2021-05-27 v0.4 <axel.hahn@iml.unibe.ch> FIX first install # 2021-07-08 v0.5 <axel.hahn@iml.unibe.ch> added function "runas" # 2021-11-01 v0.6 <axel.hahn@iml.unibe.ch> save config diffs # 2021-11-02 v0.7 <axel.hahn@iml.unibe.ch> delete logs keping N files # 2022-11-24 v0.8 <axel.hahn@iml.unibe.ch> tar -xzf without dot as 2nd param # 2022-11-25 v0.9 <axel.hahn@iml.unibe.ch> support custom phase + file per project # 2023-02-14 v1.0 <axel.hahn@unibe.ch> set v1.0 (no changes) # 2023-12-?? v1.1 <axel.hahn@unibe.ch> show OK message in profile log # 2023-12-14 v1.2 <axel.hahn@unibe.ch> export some vars; abort on errors # ====================================================================== # ---------------------------------------------------------------------- # CONFIG # ---------------------------------------------------------------------- cd $( dirname $0 ) _version=1.2 export selfdir; selfdir=$( /bin/pwd ) export profiledir tmpdir=/var/tmp/imldeployment_packages logdir=/var/log/imldeployment-client # keep last N logs per project typeset -i iKeep=10 wait=0 # wait=1 # export variables that will be set in getfile config or project export IMLCI_PROJECT=TODO export IMLCI_PHASE=TODO export cfgdiff=TODO # ---------------------------------------------------------------------- # FUNCTIONS # ---------------------------------------------------------------------- # get a list profiles by searching a config.sh # no param function getprofiles(){ find ${selfdir}/profiles/ -name "config.sh" | rev | cut -f 2 -d "/" | rev } # set a profile, load it, verify required parameters # param string name of a subdir in ./profiles/ function setprofile(){ profile=$1 # source config for software download - as default. . ${selfdir}/bin/getfile.sh.cfg # my install dir installdir= # fileowner appowner= profiledir=${selfdir}/profiles/${profile} . ${profiledir}/config.sh || exit 11 echo "[${profiledir}/config.sh] was loaded." if [ -z "$installdir" -o -z "${IMLCI_PHASE}" -o -z "${IMLCI_PROJECT}" ]; then echo "to be defined in ${profiledir}/config.sh:" echo "installdir = $installdir" echo "These variables must be set in bin/getfile.sh.cfg or in [profile]/config.sh:" echo "IMLCI_PHASE = $IMLCI_PHASE" echo "IMLCI_PROJECT = $IMLCI_PROJECT" exit 12 fi echo "OK, profile [${profile}] was set." local localfile if [ -n "$IMLCI_FILE" ]; then localfile="${IMLCI_PROJECT}__${IMLCI_FILE}" else IMLCI_FILE="${IMLCI_PROJECT}.tgz" localfile="${IMLCI_FILE}" fi downloadfile="${tmpdir}/${localfile}" downloadtmp="${tmpdir}/${localfile}.tmp" cfgdiff="${tmpdir}/${localfile}_cfgdiff.txt" test -f "${cfgdiff}" && rm -f "${cfgdiff}" } # output a colored infoline with date and given message # param string message text function header(){ local COLOR="\033[34m" local NO_COLOR="\033[0m" echo echo -en "${COLOR}" echo ______________________________________________________________________ echo -n ">>>>>>>>>> $(date) " test ! -z "$profile" && echo -n "${profile} :: " echo -n "$*" echo -en "${NO_COLOR}" if [ "$wait" = "1" ]; then echo -n " RETURN"; read dummy; fi echo } # run a command as another posix user (even if it does not have a shell) # to be used in taskas_*install.sh # # example: # runas www-data "./hooks/ondeploy" # # param string username # param string command to execute. Needs to be quoted. # param string optional: shell (default: /bin/sh) function runas(){ local _user=$1 local _cmd=$2 local _shell=$3 test -z "$_shell" && _shell=/bin/sh su $_user -s $_shell -c "$_cmd" } # execute a task/ hook - if the given task script exists and has executable # persmissions; if not it is not an error # param string filename function run_task(){ local taskscript=$1 if [ -x "${taskscript}" ]; then echo "INFO: starting script ${taskscript}..." . "${taskscript}" || exit 10 echo "DONE: script ${taskscript}" else test -f "${taskscript}" && ( echo "SKIP: task script ${taskscript} is not executable." ; ls -l "${taskscript}") test -f "${taskscript}" || echo "SKIP: task script ${taskscript} does not exist." fi } function deploy(){ local dlparams skipmessage="SKIP: no newer download file. You can use parameter -f to force reinstall." # ---------------------------------------------------------------------- header "Set profile [$1]" setprofile $1 # ---------------------------------------------------------------------- header "Download ${IML} ${IMLCI_PROJECT}.tgz" typeset -i local isupdate=$defaultupdate # getfile.sh reads phase from its cfg file - we need to add it as parameter test -n "${IMLCI_PHASE}" && dlparams="$dlparams -e ${IMLCI_PHASE}" # set the filename to fetch test -n "$IMLCI_FILE" || dlparams="$dlparams -f ${IMLCI_PROJECT}.tgz" test -n "$IMLCI_FILE" && dlparams="$dlparams -f $IMLCI_FILE" ${selfdir}/bin/getfile.sh ${dlparams} -o ${downloadtmp} if [ $? -ne 0 ]; then echo Download failed. echo Repeating request with debug param -d to get the error... # added sleep to repeat the request with another hashed secret sleep 2 ${selfdir}/bin/getfile.sh -d ${dlparams} -o ${downloadtmp} exit 1 fi # ---------------------------------------------------------------------- header "Detect if download is newer than last download." if [ -f ${downloadfile} ]; then # ls -l "${downloadfile}" "${downloadtmp}" diff "${downloadfile}" "${downloadtmp}" if [ $? -eq 0 ]; then echo "INFO: the downloaded file is the same like last download." rm -f "${downloadtmp}" else echo "OK: donwload contains an update." isupdate=$isupdate+1 mv "${downloadtmp}" "${downloadfile}" fi else echo "INFO: last download not available - first install or a forced update." isupdate=$isupdate+1 mv "${downloadtmp}" "${downloadfile}" fi ls -l "${downloadfile}" # ---------------------------------------------------------------------- header "Switch into install dir ${installdir} ..." test -d "${installdir}" || mkdir -p "${installdir}" cd ${installdir} || exit 2 ls -1 * >/dev/null 2>&1 if [ $? -ne 0 ]; then echo "INFO: target directory is empty." isupdate=$isupdate+1 fi # ---------------------------------------------------------------------- header "PRE tasks" # what you could do here: # - enable maintenance flag # - stop service # - cleanup directory ... up to remove all current files test $isupdate -eq 0 && echo $skipmessage test $isupdate -eq 0 || run_task "${profiledir}/tasks_preinstall.sh" # ---------------------------------------------------------------------- header "PRE tasks II - cleanup" if [ $isupdate -eq 0 ]; then echo $skipmessage else test "$cleanup_preview" -eq "1" || echo "SKIP: preview of cleanup is disabled." test "$cleanup_preview" -eq "1" && "${selfdir}/bin/preinstall_cleanup.sh" "${installdir}" "${downloadfile}" test "$cleanup_force" -eq "1" || echo "SKIP: cleanup files is disabled." test "$cleanup_force" -eq "1" && "${selfdir}/bin/preinstall_cleanup.sh" "${installdir}" "${downloadfile}" "force" fi # ---------------------------------------------------------------------- header "Extract ${downloadfile} in $( pwd )" test $isupdate -eq 0 && echo $skipmessage test $isupdate -eq 0 || tar -xzf "${downloadfile}" || exit 3 ls -l # ---------------------------------------------------------------------- # header "Remove download archive ${IMLCI_PROJECT}.tgz" # echo rm -f ${IMLCI_PROJECT}.tgz # ---------------------------------------------------------------------- header "Update config files" echo "Showing replacements:" ; grep '@replace\[' hooks/templates/* run_task "${profiledir}/tasks_config.sh" # ---------------------------------------------------------------------- header "Set file owner [${appowner}]" if [ $isupdate -eq 0 ]; then echo $skipmessage else if [ ! -z "${appowner}" ]; then # . is ${installdir} sudo chown -R $appowner . || exit 5 ls -la else echo "SKIP: variable appowner was not set" fi fi # ---------------------------------------------------------------------- header "POST tasks" # what you could do here: # - start current deploy scripts # - apply database updates # - set permissions # - start service # - remove maintenance flag # - send success message as email/ slack/ [another fancy tool] test $isupdate -eq 0 && echo $skipmessage test $isupdate -eq 0 || run_task "${profiledir}/tasks_postinstall.sh" hasfilechange=0 grep . $cfgdiff && hasfilechange=1 if [ $hasfilechange -eq 1 ]; then echo "INFO: a config file was created or changed." else echo SKIP: No config file was changed. fi if [ $isupdate -ne 0 -o $hasfilechange -eq 1 ]; then run_task "${profiledir}/tasks_postchange.sh" fi cd $( dirname $0 ) header "Installer Status" echo "OK: ${IMLCI_PROJECT}" } # delete old logfiles keeping the last N files # param string name of project function logdelete(){ local sProfile=$1 header "DELETE LOGS ${logdir}/${sProfile}__* ... keep $iKeep" # order files by time typeset -i local _iFiles=$( ls -1t ${logdir}/${sProfile}__*.log | wc -l ) typeset -i local _iStart=$iKeep+1 if [ $_iFiles -gt $iKeep ]; then ls -1t ${logdir}/${sProfile}__*.log | sed -n "${_iStart},${_iFiles}p" | while read mylogfile do echo -n "deleting " ls -l "${mylogfile}" && rm -f "${mylogfile}" done else echo SKIP: deletion ... less than $iKeep files found fi } # ---------------------------------------------------------------------- # MAIN # ---------------------------------------------------------------------- cd $( dirname $0 ) action="deploy" typeset -i defaultupdate=0 echo "_______________________________________________________________________________ IML - DEPLOYMENT CLIENT DOCS: https://os-docs.iml.unibe.ch/imldeployment-client/ _____ _________________________________________________________________________/ v$_version " while getopts 'hfl' arg; do case ${arg} in h) echo "HELP:" echo " Loads one or more profiles profile to deploy an application." echo " If the download file is not newer then it does not extract files and does not" echo " Optionally it cleans up the target directory." echo " Runs pre and post hooks - it updates the config files only and sets the owner." echo echo "SYNTAX:" echo " $( basename $0 ) [OPTION] [PROFILE(S)]" echo echo "OPTIONS:" echo " -h | show this help and exit" echo " -f | force full installation even if the download file is not newer" echo " -l | list exiting profile names" echo echo "PROFILE(S):" echo " Set one or more valid profile names. By default it loops over all profiles." echo " This prameter limits the execution to the given profiles." echo " Use option -l to get a list of profiles." echo exit 0 ;; f) echo "FORCE update" defaultupdate=1 shift 1 ;; l) echo "LIST of existing profiles:" getprofiles echo exit 0 ;; ?) echo "Invalid option: -${OPTARG}." exit 2 ;; esac done if [ $# -eq 0 ]; then header "looping over all profiles" getprofiles echo allprofiles=$( getprofiles ) else allprofiles="$*" fi test -d "${logdir}" || mkdir -p "${logdir}" for myprofile in $allprofiles do ( deploy $myprofile; logdelete $myprofile ) 2>&1 | tee ${logdir}/${myprofile}__$(date +%Y-%m-%d__%H%M%S).log test ${PIPESTATUS[0]} -eq 0 || exit ${PIPESTATUS[0]} profile= done rc=$? profile= header "All done." echo exiting with statuscode $rc exit $rc # ---------------------------------------------------------------------- header "DONE :-)" # ----------------------------------------------------------------------