'How to create a granular bash script with multiple variables with ssh connections
I have the below script:
bash 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 |