#!/bin/bash # Fred Denis -- September 2019 -- http://unknowndba.blogspot.com -- fred.denis3@gmail.com # # yal.sh -- Yet Another Launcher ! # # Copy and/or execute a script and/or some commands on many hosts; can connect and sudo to execute as different user per server # Use the -h option to show the usage function and the available options # Also, detailed explanations about yal.sh are available on http://bit.ly/2lNiLLF # # The current version of the script is 20250623 # # History: # # 20250623 - Fred DENIS - Rewrote the whole after and before execution and how to get the different commands per server # - -l user is no more mandatory (indeed alias in ~/.ssh/config already specify the user to log in with) # - Cosmetic # 20191008 - Fred Denis - -a and -b support different parameters values per server: server1:date,server2:uptime # 20191002 - Fred Denis - Implemented the "user_to_log:user_to_exec@server" syntax # 20190912 - Fred Denis - Initial release # # # Default values # # You can update the below default values (variables starting with DEFAULT_) but I recommend using a configuration file (see variable CONFIG_FILE) # DEFAULT_AFTER="" # Default command to execute after the script DEFAULT_BEFORE="" # Default command to execute before the script DEFAULT_SERVER_LIST="" # List of hosts to execute the script on DEFAULT_DEST="/tmp" # Target directory to copy the script before executing it DEFAULT_USER_TO_EXEC="" # User used to execute the script on the target server (sudo to this user privilege is needed) DEFAULT_FILE_TO_COPY="" # A file to copy to the target servers DEFAULT_GROUP_FILE="" # A file to copy to the target servers DEFAULT_SCRIPT="" # A file containing a list of target servers (1 server per line) DEFAULT_USER_TO_LOG="" # User used to connect to the target server DEFAULT_QUIET="NO" # Show the temporary file copy and remove when we execute a file on a remote server DEFAULT_SCRIPT_OPTIONS="" # Default options to append to a script we execute HEADER="echo BEGIN on \$(hostname -s): \$(date)" # Header to print before execution on target FOOTER="echo END\ \ \ on \$(hostname -s): \$(date)" # Footer to print after execution on a target CONFIG_FILE=".yal.config" # Default config file containing default values overwritting these ones SHOW_OPTIONS="no" # Show the options that would be used and exit -- do not do anything else (-o) SHOW_ELAPSED="yes" # Show the elapsed time DEFAULT_SSH_OPTIONS="-qT" # SSH options when connecting to the hostsa (you may not want to modify this one) DEFAULT_SCP_OPTIONS="-q" # SCP options when there is a file to copy and/or execute (you may not want to modify this one) # # List of parameters that can be modified (config file, top of the script or command line option) # list_param=('AFTER' 'BEFORE' 'SERVER_LIST' 'DEST' 'USER_TO_EXEC' 'FILE_TO_COPY' 'GROUP_FILE' 'USER_TO_LOG' 'SCRIPT' 'SSH_OPTIONS' 'SCP_OPTIONS' 'QUIET' 'SCRIPT_OPTIONS') # # Colors used for the header, footer and elapsed time # BLUE_BOLD="1;34m" BLUE="34m" COLOR="${BLUE_BOLD}" # # Function to get values from the config file, receive a pattern and return the value specified in the config file # Form should be : PATTERN=value or PATTERN="list of values" # get_from_config_file() { A=$(grep "$1" "${CONFIG_FILE}" | grep -v "^#" | sed s'/^[[:space:]]*//g' | sed s'/#.*$//' | sed s'/^.*=[[:space:]]*//' | sed s'/[[:space:]]*$//' | sed s'/"//g') echo "$A" } # # Get the values from the config file # if [[ -f "${CONFIG_FILE}" ]]; then for name in "${list_param[@]}"; do declare -x "$name"="$(get_from_config_file "$name")" done fi # # Show the version of the script (-V) # show_version() { VERSION=$(awk '{if ($0 ~ /^# 20[0-9][0-9][0-1][0-9]/) {print $2; exit}}' $0) printf "\n\t\033[1;36m%s\033[m\n\n" "The current version of "$(basename $0)" is "$VERSION"." ; } fill_command_array() { # # Fill an array with the server as key and the command as values in the case of syntaxes like this: # server1:date;id,server2"uptime # local command_string="$1" local array_name="$2" declare -n out_array="$array_name" # Nameref to assign to actual array shift 2 local servers=("$@") if [[ "$command_string" == *:* ]]; then # Host-specific commands for H in "${servers[@]}"; do cmd=$(echo "$command_string" | tr ',' '\n' | grep "^$H:" | sed "s/^$H://") out_array["$H"]="$cmd" done else # Same command for all for H in "${servers[@]}"; do out_array["$H"]="$command_string" done fi } # # Use default values if not specified in the config file # if [[ -z "${AFTER}" ]] ; then AFTER="$DEFAULT_AFTER" ; fi if [[ -z "${BEFORE}" ]] ; then BEFORE="$DEFAULT_BEFORE" ; fi if [[ -z "${SERVER_LIST}" ]] ; then SERVER_LIST=$DEFAULT_SERVER_LIST ; fi if [[ -z "${DEST}" ]] ; then DEST=$DEFAULT_DEST ; fi if [[ -z "${USER_TO_EXEC}" ]] ; then USER_TO_EXEC=$DEFAULT_USER_TO_EXEC ; fi if [[ -z "${FILE_TO_COPY}" ]] ; then FILE_TO_COPY=$DEFAULT_FILE_TO_COPY ; fi if [[ -z "${GROUP_FILE}" ]] ; then GROUP_FILE=$DEFAULT_GROUP_FILE ; fi if [[ -z "${USER_TO_LOG}" ]] ; then USER_TO_LOG=$DEFAULT_USER_TO_LOG ; fi if [[ -z "${SCRIPT}" ]] ; then SCRIPT=$DEFAULT_SCRIPT ; fi if [[ -z "${SSH_OPTIONS}" ]] ; then SSH_OPTIONS="$DEFAULT_SSH_OPTIONS" ; fi if [[ -z "${SCP_OPTIONS}" ]] ; then SCP_OPTIONS="$DEFAULT_SCP_OPTIONS" ; fi if [[ -z "${QUIET}" ]] ; then QUIET="$DEFAULT_QUIET" ; fi if [[ -z "${SCRIPT_OPTIONS}" ]] ; then SCRIPT_OPTIONS="$DEFAULT_SCRIPT_OPTIONS" ; fi # # An usage function # usage() { printf "\n\033[1;37m%-8s\033[m\n" "NAME" ; cat << END $(basename $0) - Execute or copy a script and/or commands on many hosts END printf "\n\033[1;37m%-8s\033[m\n" "SYNOPSIS" ; cat << END $0 [-a] [-b] [-c] [-d] [-e] [-f] [-g] [-l] [-o] [-q] [-Q] [-x] [-s] [-S] [-h] [-V] END printf "\n\033[1;37m%-8s\033[m\n" "DESCRIPTION" ; cat << END $(basename $0) needs SSH equivalence already set between the user executing it and the target user (option -l) $(basename $0) can also sudo to a user to execute a script (option -e) $(basename $0) supports a "user_to_log:user_to_exec@server" syntax to connect and sudo as different user per server With no option, $(basename $0) use the values defined in the config file and if not, the default values from the top of the script Options precedence is: 1/ Option through the command line 2/ Defaults options defined on top of the script (variables DEFAULT_xxxx) 3/ Options defined in the config file (if exists) END printf "\n\033[1;37m%-8s\033[m\n" "OPTIONS" ; cat << END -a Commands to execute before copying and/or executing the script -b Commands to execute after copying and/or executing the script -c Comma separated list of target servers to log in to -g Specify a file containing a list of target servers to connect to (1 server per line) The comma separated list (-c), the list of servers in a file (-g) and the SERVER_LIST parameter in the config file also support a "user_to_log:user_to_exec@server" syntax to bypass the user to login (-l) and the user to exec (-e) for specific servers; please find an example below: $ $(basename $0) -c root:oracle@server1,:oragrid@server2,opc@server3,server4 -l oracle -e grid -x asmdu.sh this will be resolved as: root:oracle@server1 => ssh root @server1 then sudo oracle to execute asmdu.sh :oragrid@server2 => ssh oracle (from -l) @server2 then sudo oragrid to execute asmdu.sh opc@server3 => ssh opc @server3 then sudo grid (from -e) to execute asmdu.sh server4 => ssh oracle (from -l) @server4 then sudo grid (from -e) to execute asmdu.sh -d Destination of the file on the target servers -e User to use to Execute the script and the commands on the target server (sudo to this user privilege is needed) -f File to be copied to the target servers (see -x to have the file also executed) -l User to use to Login to the target servers -o Only shows the values of the options and exit (do not do anything else) -q Do not show the copy and remove temporary file operations when executing a script -Q Show the copy and remove temporary file operations when executing a script -s SSH options -S SCP options -x Copy and eXecute the script on the target hosts (see -f to only copy a file) -V Show the version -h Shows this help Experiment and enjoy ! END exit 123 } # # Print a line of "-" # print_a_line() { printf "\n" ; for i in $(seq 1 $1); do printf "\033[1;37m%1s\033[m" "-" ; done } # # Options (overwrite the default values) # while getopts "a:b:c:d:e:f:g:l:ox:hs:S:VqQO:" OPT; do case ${OPT} in a) AFTER=${OPTARG} ;; b) BEFORE=${OPTARG} ;; c) SERVER_LIST=${OPTARG} ;; d) DEST=${OPTARG} ;; e) USER_TO_EXEC=${OPTARG} ;; f) FILE_TO_COPY=${OPTARG} ;; g) GROUP_FILE=${OPTARG} ;; l) USER_TO_LOG=${OPTARG} ;; o) SHOW_OPTIONS="yes" ;; O) SCRIPT_OPTIONS=${OPTARG} ;; s) SSH_OPTIONS=${OPTARG} ;; q) QUIET="YES" ;; Q) QUIET="NO" ;; S) SCP_OPTIONS=${OPTARG} ;; x) SCRIPT=${OPTARG} ;; V) show_version; exit 555 ;; h) usage ;; \?) echo "Invalid option: -$OPTARG" >&2; usage ;; esac done if [[ -n "${GROUP_FILE}" && -n "${SERVER_LIST}" ]]; then printf "\n\033[1;33m%s\033[m\n\n" "Info: when both a server list (-c) and a group file (-g) are specified, the group file is used." ; fi if [[ -n "${GROUP_FILE}" ]]; then # A group file is specified, we will use the hosts from it if [[ -f "${GROUP_FILE}" ]]; then SERVER_LIST=$(cat "${GROUP_FILE}" | awk '{printf("%s,",$1)}' | sed s'/,$//') else printf "\n\t\033[1;31m%s\033[m\n\n" "Cannot find ${GROUP_FILE}, cannot continue." ; exit 669 fi fi # # Show the value of each option with the current setting -- do not do anything, just show and exit # if [[ "${SHOW_OPTIONS}" = "yes" ]]; then # Adapt the colum size to the longest value of the variales COL2=${#CONFIG_FILE} for X in "${list_param[@]}"; do A="${!X}" ; if (( ${#A} > $COL2 )); then COL2=$(( ${#A}+1 )) ; fi done SIZE=$(( 24 + ${COL2} )) ; FORMAT_TITLE="\n\033[1;37m%-20s\033[m\033[1;37m| %-${COL2}s |\033[m" ; FORMAT_VALUE="\n\033[1;34m%-20s\033[m\033[1;37m|\033[m %-${COL2}s \033[1;37m|\033[m" ; printf "${FORMAT_TITLE}" "Option" "Value" ; print_a_line $SIZE ; printf "${FORMAT_VALUE}" "-a: After" "$AFTER" ; printf "${FORMAT_VALUE}" "-b: Before" "$BEFORE" ; printf "${FORMAT_VALUE}" "-c: Servers List" "$SERVER_LIST" ; printf "${FORMAT_VALUE}" "-d: Dest" "$DEST" ; printf "${FORMAT_VALUE}" "-e: User to exec" "$USER_TO_EXEC" ; printf "${FORMAT_VALUE}" "-f: File to copy" "$FILE_TO_COPY" ; printf "${FORMAT_VALUE}" "-g: Group File" "$GROUP_FILE" ; printf "${FORMAT_VALUE}" "-l: User to login" "$USER_TO_LOG" ; printf "${FORMAT_VALUE}" "-q: Quiet" "$QUIET" ; printf "${FORMAT_VALUE}" "-s: SSH Options" "$SSH_OPTIONS" ; printf "${FORMAT_VALUE}" "-S: SCP Options" "$SCP_OPTIONS" ; printf "${FORMAT_VALUE}" "-x: Script to exec" "$SCRIPT" ; printf "${FORMAT_VALUE}" "-O: Script options" "$SCRIPT_OPTIONS" ; printf "${FORMAT_VALUE}" " Config File" "$CONFIG_FILE" ; print_a_line $SIZE ; printf "\n\n" ; exit 555 fi # # Input checks # if [[ -n "${SCRIPT}" && ! -f "${SCRIPT}" ]]; then printf "\n\t\033[1;31m%s\033[m\n\n" "Cannot find ${SCRIPT}, cannot continue." ; exit 666 fi if [[ -n "${FILE_TO_COPY}" && ! -f "${FILE_TO_COPY}" ]]; then printf "\n\t\033[1;31m%s\033[m\n\n" "Cannot find ${FILE_TO_COPY}, cannot continue." ; exit 667 fi if [[ -n "${SCRIPT}" ]]; then FILE_TO_COPY=${SCRIPT} fi if [[ -z "${SERVER_LIST}" ]]; then printf "\n\t\033[1;31m%s\033[m\n\n" "A server to connect to is needed (-c or -g option), cannot continue." ; exit 670 fi # # Getting ready # TO=${DEST}"/"${FILE_TO_COPY} # for better visibility below if [[ -n ${SCRIPT} ]]; then TO=${DEST}"/"${FILE_TO_COPY}$(date -u "+%s")$$ # A temporary name for the copied script if we execute the script fi servers=(${SERVER_LIST//,/ }) # Split every server in an array # # Each AFTER and BEFORE command in arrrays # declare -A commands_before declare -A commands_after fill_command_array "$BEFORE" commands_before "${servers[@]}" fill_command_array "$AFTER" commands_after "${servers[@]}" # # Let's go # for X in ${servers[@]}; do # # Management of the "user_to_log:user_to_exec@server" syntax # if [[ ! $X =~ "@" ]] && [[ ! $X =~ ":" ]]; then X=":@"$X fi if ! [[ $X =~ ":" ]]; then X=`echo $X | sed s'/@/:@/'` fi LOGIN=$(echo $X | awk -F "[:@]" '{print $1}') EXEC=$(echo $X | awk -F "[:@]" '{print $2}') SERVER=$(echo $X | awk -F "[:@]" '{print $3}') if [[ -z "${LOGIN}" ]]; then LUSER="${USER_TO_LOG}" ; else LUSER="${LOGIN}" ; fi if [[ -z "${EXEC}" ]]; then XUSER="${USER_TO_EXEC}" ; else XUSER="${EXEC}" ; fi if [[ -n "${LUSER}" ]]; then SSH_USER="-l ${LUSER}" ; SCP_USER="${LUSER}" ; fi # # Elaspsed time # if [[ "${SHOW_ELAPSED}" = "yes" ]]; then START_TIME="$(date -u +%s)" fi # # Header and Before # if [[ -n "${HEADER}" || -n "${XBEFORE}" ]]; then ssh ${SSH_OPTIONS} ${SSH_USER} ${SERVER} << END_SSH if [[ -n "${HEADER}" ]]; then echo -ne "\e[${COLOR}" ; eval "${HEADER}" ; echo -ne "\e[0m" ; fi if [[ -n "${commands_before[$SERVER]}" ]]; then if [[ -n "${XUSER}" ]]; then sudo su - ${XUSER} << END_SU eval "${commands_before[$SERVER]}" ; END_SU else eval "${commands_before[$SERVER]}" ; fi fi END_SSH fi # # Script copy # if [[ -f "${FILE_TO_COPY}" ]]; then # A file to copy scp ${SCP_OPTIONS} ${FILE_TO_COPY} ${SCP_USER}@${SERVER}:${TO} RET=$? if [[ "${QUIET}" == "NO" ]]; then if [ $RET -eq 0 ]; then printf "%s\n" "Script "${FILE_TO_COPY}" successfully copied to "${TO} ; else printf "%s\n" "Error "$?" when copying "${FILE_TO_COPY}" to "${TO} ; fi fi fi # # Script execution, After and Footer # if [[ -n "${SCRIPT}" || -n "${XAFTER}" || -n "${FOOTER}" ]]; then ssh ${SSH_OPTIONS} ${SSH_USER} ${SERVER} << END_SSH if [[ -n "${SCRIPT}" ]]; then chmod 777 ${TO} if [[ -n "${XUSER}" ]]; then sudo su - ${XUSER} << END_SU . ${TO} ${SCRIPT_OPTIONS} ; END_SU else . ${TO} ${SCRIPT_OPTIONS} ; fi if [[ -f "${TO}" ]]; then rm -f ${TO} ; if [[ "${QUIET}" == "NO" ]]; then if [ $? -eq 0 ]; then printf "%s\n" "Script "${TO}" removed successfully" ; else printf "%s\n" "Error "$?" when removing "${TO} ; fi fi fi fi if [[ -n "${commands_after[$SERVER]}" ]]; then if [[ -n "${XUSER}" ]]; then sudo su - ${XUSER} << END_SU eval "${commands_after[$SERVER]}" ; END_SU else eval "${commands_after[$SERVER]}" ; fi fi if [[ -n "${FOOTER}" ]]; then echo -ne "\e[${COLOR}" ; eval "${FOOTER}" ; echo -ne "\e[0m" ; fi END_SSH fi if [[ "${SHOW_ELAPSED}" = "yes" ]]; then END_TIME="$(date -u +%s)" ; ELAPSED="$(($END_TIME-$START_TIME))" ; printf "\033[${COLOR}%-8s: %s\033[m\n" "ELAPSED" "$ELAPSED seconds" ; fi done #************************************************************************# # E N D O F S O U R C E *# #************************************************************************#