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

Viewed 169

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?

5 Answers

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.

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]}

 

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]}
...

Here is how I do remote data collection using ssh

Some remarks

  • As this function is intended to collect datas, not to connect host (as they don't stay connected), I've renamed them ;-)

  • Avoid useless forks! use printf -v varname ... instead of varname=$(printf ...)

  • Moved STDERR redirection 2>/dev/null at more suited place.

  • use local to reserve variables names into function scope.

  • printf must have correct number of arguments, corresponding to number of %s see This format string has 3 variable....

  • Don't use -i switch to grep if not needed!

remote_collect() {
    local target_host=$1 host_answer rhelInfo perlInfo
    {
        read -r host_answer
        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
        hostname
        awk 'END{print $7}' </etc/redhat-release
        rpm -qa | grep mod_perl
EOF
    ) 2>/dev/null
    if [[ $? -eq 0 ]] ;then
        printf "| %-10s | %-14s | %-29s |\n" \
            "host_answer" "$rhelInfo" "$perlInfo"
    else
        printf "| %-10s | %-46s |\n" \
            "target_host" "Unable to get the ssh connection"
    fi
}

Notice: the EOF mark could be preceded by tabulations Tab, but not spaces! I've dropped tabulation before posting to ensure they won't be converted in space during cut'n paste.

And the head line could be

printf -v marker '%61s' ''
marker=${marker// /-}
printf "+%s+\n| %-10s | %-14s | %-29s |\n+%s+\n" \
   "$marker" "Hostname" "RedHat Version" "Perl Version" "$marker" 
+-------------------------------------------------------------+
| Hostname   | RedHat Version | Perl Version                  |
+-------------------------------------------------------------+
| hostok     | 6.3            | mod_perl-2.0.4-10.el6_5.armf  |
| hostbad    | Unable to get the ssh connection               |

Pretty table using UFT-8 borders

printf -v sl %31s '';sl=${sl// /$'\U2500'}
printf '%b%-12s%b%-16s%b%-31s%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

then:

    if [[ $? -eq 0 ]] ;then
        printf "\U2502 %-10s \U2502 %-14s \U2502 %-29s \U2502\n" \
            "$host_answer" "$rhelInfo" "$perlInfo"
    else
        printf "\U2502 %-10s \U2502 %-46s \U2502\n" \
            "$target_host" "      Unable to get the ssh connection"
    fi

and finally

# 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"

should produce:

┌────────────┬────────────────┬───────────────────────────────┐
│ Hostname   │ RedHat Version │ Perl Version                  │
├────────────┼────────────────┼───────────────────────────────┤
│ hostok     │ 6.3            │ mod_perl-2.0.4-10.el6_5.armf  │
│ hostbad    │       Unable to get the ssh connection         │
└────────────┴────────────────┴───────────────────────────────┘

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 │
Related