'How to create a granular bash script with multiple variables with ssh connections

I have the below script:

Script:

#!/bin/bash
###########
printf "\n"
marker=$(printf "%0.s-" {1..60})
printf "|$marker|\n"
printf "|%-10s | %-13s | %-29s |\n" "Hostname" "RedHat Vesrion" "Perl Version"
printf "|$marker|\n"

remote_connect() {
   target_host=$1
   marker=$(printf "%0.s-" {1..60})
   rhelInfo=$(ssh -i /home/zabbix/.ssh/ssh_key "root@${target_host}" -o StrictHostKeyChecking=no -o PasswordAuthentication=no cat /etc/redhat-release| awk 'END{print $7}')
   perlInfo=$(ssh -i /home/zabbix/.ssh/ssh_key "root@${target_host}" -o StrictHostKeyChecking=no -o PasswordAuthentication=no "rpm -qa | grep -i mod_perl")
   if [[ $? -eq 0 ]]
   then
     printf "|%-10s | %-13s | %-20s |\n" "$target_host" "$rhelInfo" "$perlInfo"
   else
     printf "|%-10s | %-13s | %-20s |\n" "$target_host" "Unable to get the ssh connection"
fi
}  2>/dev/null
export -f remote_connect
< /home/zabbix/hostsList.txt  xargs -P30 -n1 -d'\n' bash -c 'remote_connect "$@"' --

The above script runs pretty well for me while running in parallel mode.

Script results:

|------------------------------------------------------------|
|Hostname   | RedHat Vesrion | Perl Version                  |
|------------------------------------------------------------|
|foxnl41    | 6.9           | mod_perl-2.0.4-11.el6_5.x86_64 |
|foxnl84    | 6.9           | mod_perl-2.0.4-11.el6_5.x86_64 |
|foxnl42    | 6.9           | mod_perl-2.0.4-11.el6_5.x86_64 |
|foxnl63    | 6.9           | mod_perl-2.0.4-11.el6_5.x86_64 |
|foxnl10    | 6.7           | mod_perl-2.0.4-11.el6_5.x86_64 |
|foxnl55    | 6.9           | mod_perl-2.0.4-11.el6_5.x86_64 |
|foxnl95    | 6.9           | mod_perl-2.0.4-11.el6_5.x86_64 |
|foxnl85    | 6.9           | mod_perl-2.0.4-11.el6_5.x86_64 |

Concern ?

I have two variables: rhelInfo and perlInfo to get store information. But it is using two ssh calls to the servers to get the values.

Could I have only one SSH call to execute multiple commands and set both variables?



Solution 1:[1]

Could I have only one SSH call to execute multiple commands and set both variables?

Sure. You could do:

# this looks too long - so a function
_ssh() {
  ssh -i /home/zabbix/.ssh/ssh_key \
      -o StrictHostKeyChecking=no \
      -o PasswordAuthentication=no \
      "root@$target_host" \
      "$@"
}
export -f _ssh

# the function to-be-executed on the remote
remotework() {
    rhelinfo=$(awk 'END{print $7}' /etc/redhat-release)
    perlinfo=$(rpm -qa | grep -i mod_perl)
    # output elements separated by byte 0x01
    printf "%s\001" "$rhelinfo" "$perlinfo"
}
export -f remotework

remote_connect() {
    # execute bash on the remote
    # with `remotework` function serialized
    # and execute the `remotework` function
    # properly `printf %q` quote everything for unquoting done by ssh+remote shell
    tmp=$( _ssh "$(printf "%q " bash -c "$(declare -f remotework); remotework")" )
    # split the output of ssh by byte 0x01
    {
       IFS= read -d $'\x01' -r rhelInfo &&
       IFS= read -d $'\x01' -r perlInfo
    } <<<"$tmp"
}

or similar variation of it. Basically ssh gives you a bidirection stream of data - you can stream anything with a separator in between ("custom protocol") and then split the data on that separator. I.e. the problem is not limited to ssh - research data serialization/deserialization in bash. Above I have chosen the byte 0x01 to be the separator - you can use a separate line with unique uuid, use base64 -w0 to convert data to a single line, or similar or use another format.

remotework() {
    awk ... | base64 -w0
    echo ' '
    rpm ... | base64 -w0
    echo
}
...
   IFS=' ' read -r rhelInfo perlInfo <<<"$tmp"
   rhelInfo=$(<<<"$rhelInfo" base64 -d)
   perlInfo=$(<<<"$perlInfo" base64 -d)

You can also serialize variables with declare -p and then eval them - this is in my opinion more dangerous, so it's better to use a separator.

Solution 2:[2]

Suggesting to replace following lines:

   rhelInfo=$(ssh -i /home/zabbix/.ssh/ssh_key "root@${target_host}" -o StrictHostKeyChecking=no -o PasswordAuthentication=no cat /etc/redhat-release| awk 'END{print $7}')
   perlInfo=$(ssh -i /home/zabbix/.ssh/ssh_key "root@${target_host}" -o StrictHostKeyChecking=no -o PasswordAuthentication=no "rpm -qa | grep -i mod_perl")
   if [[ $? -eq 0 ]]; then
     printf "|%-10s | %-13s | %-20s |\n" "$target_host" "$rhelInfo" "$perlInfo"
   else
     printf "|%-10s | %-13s | %-20s |\n" "$target_host" "Unable to get the ssh connection"
   fi

With following lines:

   sshResponseArr=( $(ssh -i /home/zabbix/.ssh/ssh_key "root@${target_host}" -o StrictHostKeyChecking=no -o PasswordAuthentication=no "awk 'END{print}'  /etc/redhat-release; rpm -aq|grep -i mod_perl") ) 
   if [[ $? -eq 0 ]]; then
     printf "|%-10s | %-13s | %-20s |\n" "$target_host" "${sshResponseArr[6]}" "${sshResponseArr[8]}"
   else
     printf "|%-10s | %-13s | %-20s |\n" "$target_host" "Unable to get the ssh connection"
   fi

Explanation

First I call multi-line commands in the ssh request. Placing all the commands in a single argument with " (as you did).

The real trick is to read multi-line ssh response into bash array variable sshResponseArr

By default the array is parsed by spaces.

I assume (as you did) that words positioning in the array is consistent across all hosts therefore:

$rhelInfo is ${sshResponseArr[6]}

And $perlInfo is ${sshResponseArr[8]}

Alternative conservative handling of sshResponseArr as an array of 2 lines response from ssh command:

 mapfile -t sshResponseLinesArr < <(ssh -i /home/zabbix/.ssh/ssh_key "root@${target_host}" -o StrictHostKeyChecking=no -o PasswordAuthentication=no "awk 'END{print}'  /etc/redhat-release; rpm -aq|grep -i mod_perl")
 lastLineInReleaseFile=${sshResponseLinesArr[0]}
 mod_perl_response=${sshResponseLinesArr[1]}

 

Solution 3:[3]

You can run both commands in one ssh run and then parse the results, like this for example:

...
remote_data=($(
    ssh -i /home/zabbix/.ssh/ssh_key "root@${target_host}" -o StrictHostKeyChecking=no -o PasswordAuthentication=no "
        rhelInfo=\$(cat /etc/redhat-release | awk 'END{print \$7}')
        perlInfo=\$(rpm -qa | grep -i mod_perl)
        
        echo \$rhelInfo \$perlInfo  
"))

rhelInfo=${remote_data[0]}
perlInfo=${remote_data[1]}
...

Explanation: remote_data=( $(ssh ...) ) - will create an array 'remote_data' and fill it with values from output of the command in $() Array will automatically split values by space, tab or new line. So it this case we will have remote_data=( 6.9 mod_perl-2.0.4-11.el6_5.x86_64 )

And then these values are assigned to variables:

rhelInfo=${remote_data[0]}
perlInfo=${remote_data[1]}

BTW echo(and cat)) is an overkill here, so this could be simplified to:

...
remote_data=($(
    ssh -i /home/zabbix/.ssh/ssh_key "root@${target_host}" \
        -o  StrictHostKeyChecking=no \
        -o PasswordAuthentication=no "
           awk 'END{print \$7}' /etc/redhat-release
           rpm -qa | grep -i mod_perl
    "
))

rhelInfo=${remote_data[0]}
perlInfo=${remote_data[1]}
...

Solution 4:[4]

I am Keeping it here after testing from my side with @F. Hauri answer..

#!/bin/bash
###########
printf -v sl %32s '';sl=${sl// /$'\U2500'}
printf '%b%-12s%b%-16s%b%-32s%b\n' \
    \\U250c "${sl::12}" \\U252c "${sl::16}" \\U252c "$sl" \\U2510 \
    \\U2502 ' Hostname' \\U2502 ' RedHat Version' \\U2502 ' Perl Version' \
        \\U2502   \\U251c "${sl::12}" \\U253c "${sl::16}" \\U253c "$sl" \\U2524

remote_collect() {
    target_host=$1
    {
        read -r rhelInfo
        read -r perlInfo
    } < <(
        ssh -i /home/zabbix/.ssh/ssh_key "root@${target_host}" \
            -o StrictHostKeyChecking=no -o PasswordAuthentication=no \
            /bin/sh <<-EOF
        cat /etc/redhat-release | awk 'END{print \$7}'
        rpm -qa | grep mod_perl
EOF
    ) 2>/dev/null

    if [[ $? -eq 0 ]] ;then
        printf "\U2502 %-10s \U2502 %-14s \U2502 %-28s \U2502\n" \
            "$target_host" "$rhelInfo" "$perlInfo"
    else
        printf "\U2502 %-10s \U2502 %-14s \U2502  %-29s \U2502\n" \
            "$target_host" "?" "Unable to connect"
    fi
 # printf -v sl %31s '';sl=${sl// /$'\U2500'} # uncomment if $sl not at main scope
 printf '\U2514%s\U2534%s\U2534%s\U2518\n' "${sl::12}" "${sl::16}" "$sl"

} 2>/dev/null
export -f remote_collect
< /home/zabbix/hostsList.txt  xargs -P30 -n1 -d'\n' bash -c 'remote_connect "$@"' --

Output 1:

????????????????????????????????????????????????????????????????
? Hostname   ? RedHat Version ? Perl Version                   ?
????????????????????????????????????????????????????????????????
? foxnl46    ? ?              ?  Unable to connect             ?
????
? foxnl27    ? 6.7            ? mod_perl-2.0.4-11.el6_5.x86_64 ?
????
? foxnl32    ? 6.7            ? mod_perl-2.0.4-11.el6_5.x86_64 ?

If i remove the last printf it all turns okay expect the last line for table.

Output 2

????????????????????????????????????????????????????????????????
? Hostname   ? RedHat Version ? Perl Version                   ?
????????????????????????????????????????????????????????????????
? foxnl46    ? ?              ?  Unable to connect             ?
? foxnl27    ? 6.7            ? mod_perl-2.0.4-11.el6_5.x86_64 ?
? foxnl32    ? 6.7            ? mod_perl-2.0.4-11.el6_5.x86_64 ?

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3
Solution 4 kulfi