-
Hahn Axel (hahn) authoredHahn Axel (hahn) authored
deploy_app.sh 13.14 KiB
#!/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
# 2023-12-18 v1.3 <axel.hahn@unibe.ch> add lines to profile log on error
# ======================================================================
# ----------------------------------------------------------------------
# CONFIG
# ----------------------------------------------------------------------
cd $( dirname $0 )
_version=1.3
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
typeset -i rc=0
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
logfile=${logdir}/${myprofile}__$(date +%Y-%m-%d__%H%M%S).log
( deploy $myprofile; logdelete $myprofile ) 2>&1 | tee "${logfile}"
lastrc=${PIPESTATUS[0]}
if [ $lastrc -ne 0 ]; then
rc+=$lastrc
echo "The rollout of $myprofile was interrupted by an exitcode $lastrc." >> "${logfile}"
echo "FAILED $myprofile" >> "${logfile}"
fi
profile=
done
profile=
header "All done."
echo exiting with statuscode $rc
exit $rc
# ----------------------------------------------------------------------
header "DONE :-)"
# ----------------------------------------------------------------------