Send complex command via ssh - bash

I'm trying to send this command via ssh:
ssh <user1>#<ip1> ssh <user2>#<ip2> /opt/user/bin -f /opt/user/slap.conf -l /home/admin/`date +%Y%m%d`_Export_file$nr.gz -s "ou=multi" -a "(& (entry=$nr)(serv=PS))" -o wrap=no
this command is customized so do not confuse with this...
But it's not executed, smth like: unexpected '(
If i log in to the server and i give this command it gets executed correctly. So i think it should be something with bracket and parentheses rules.
Please can someone help me?
thank you in advance.

You will need to escape the quotes, possibly twice, since each invocation of ssh will involve stripping a layer off. Put escaped single quotes round the entire command, and then nested unescaped single quotes round the inner command:
ssh <user1>#<ip1> \'ssh <user2>#<ip2> '/opt/user/bin -f /opt/user/slap.conf -l /home/admin/`date +%Y%m%d`_Export_file$nr.gz -s "ou=multi" -a "(& (entry=$nr)(serv=PS))" -o wrap=no'\'
This assumes, by the way, that you want the backticks to be unpacked and the command executed on ip2, rather than beforehand on your source machine, and similarly with the decoding of the $nr variable. It's not clear how you want them interpreted.

Related

Remote script not recognizing environment variable set on host machine in screen

I have a bash script that runs another script in a screen on a remote computer. The environment variable GITLAB_CI_TOKEN is set on the host machine and is defined properly. However, the script configure.sh on the remote machine tells that this environment variable is empty when it is executed, even if it is defined on the same line as the script...
Here is the command I am using:
ssh -o "StrictHostKeyChecking=accept-new" "${COMPUTERS_IPS[i]}" \
screen -S "deploy_${COMPUTERS_IPS[i]}" -dm " \
GITLAB_CI_TOKEN=${GITLAB_CI_TOKEN} \
bash \"${REMOTE_FOLDER}/configure.sh\" \"${REMOTE_FOLDER}\" > ${LOG_FILE} 2>&1;
"
Additionally, the logs are not being written to LOG_FILE, but are being displayed on the console of the screen. I have been pulling my hair out over this for the past two days... Any help or guidance would be greatly appreciated :)
Why GITLAB_CI_TOKEN is "empty":
Passing a command to a remote host over ssh is very similar to running it through eval. For example in your case, escaped newlines on the first evaluation become unescaped newlines on a subsequent evaluation. Consider this very simple program named args (place it in bin or somewhere else on your path to demo):
#!/bin/bash
for arg ; do
echo "|$arg|"
done
And these two use cases:
args "\
Hello \
World"
# prints:
# |Hello World|
ssh host args "\
Hello \
World"
# prints:
# |Hello|
# |World|
As you can see, when we run this via ssh the newline we attempted to escape splits our data into two separate lines even though we tried to keep it all on one line. This means your assignment of GITLAB_CI_TOKEN is just a regular shell variable instead of a scoped environment variable for your bash command. A scoped environment variable requires the declaration and the command to happen on the same line.
The easiest thing to do is likely to just export the variable explicitly with export GITLAB_CI_TOKEN=${GITLAB_CI_TOKEN}.
For similar reasons, the output of your command is going to the screen and not the logfile because the outer quotes of screen -dm "commands >output" are getting stripped on the first evaluation, and then the remote host is parsing screen -dm commands >output and assigning the output redirection to screen instead of commands. That means your configure.sh is writing to the screen, and it's the screen program that's writing its own output to a logfile.
To send complex commands to a remote host, you may want to look into tools like printf %q which can produce escaped output suitable for being safely evaluated in an eval-like context. Take a look at BashFAQ/096 for an example.

Why would I want to wrap my script commands in double quotes?

I'm working through Red Hat Academy and one of the sample scripts provided differed from mine in a key way that I haven't been able to understand:
#!/usr/bin/bash
#
USR='student'
OUT='/home/student/output'
#
for SRV in servera serverb
do
ssh ${USR}#${SRV} "hostname -f" > ${OUT}-${SRV}
echo "#####" >> ${OUT}-${SRV}
ssh ${USR}#${SRV} "lscpu | grep '^CPU'" >> ${OUT}-${SRV}
echo "#####" >> ${OUT}-${SRV}
ssh ${USR}#${SRV} "grep -v '^$' /etc/selinux/config|grep -v '^#'" >> ${OUT}-${SRV}
echo "#####" >> ${OUT}-${SRV}
ssh ${USR}#${SRV} "sudo grep 'Failed password' /var/log/secure" >> ${OUT}-${SRV}
echo "#####" >> ${OUT}-${SRV}
done
On each line after the variable expansion, they've wrapped most of the rest of the command in double quotes. Now, I understand the usage of double quotes and single quotes when it comes to suppressing expansion/substitution, but the main takeaway I've learned is straight from the RH Academy:
It is recommended practice to use single quotation marks to
encapsulate the regular expression to protect any shell metacharacters
(such as the $, *, and {} characters). Encapsulating the regular
expression ensures that the characters are interpreted by the intended
command and not by the shell.
I can see that they're doing that here to make sure the regular expressions are properly passed to grep in a couple of lines, but I can't figure out the purpose of the double quotes. My questions are:
Why are they there?
Why are they only wrapped around a portion of
each line?
Is this a common practice?
As pointed out by others, the example provided has issues of being malformed for the intended purpose. Specifically, the 3rd ssh requires the "$" in '^$' to be escaped, to avoid the shell's attempt at substitution at your end. Otherwise, the remote host will see '^' !!!
Their usage of double-quotes is intended to "encapsulate" a construct meant to be executed as is on the remote server, hence around everything after the target host reference.
Issues arise if you need to expand variable values before sending to remote ... AND ... when you need such expansion to NOT occur. Single-quotes are meant to contain literals meant to be passed as-is, but if that is contained within another set of double-quotes, the contents of those are not "protected" and the shell attempts the usual substitutions whenever "$" is encountered.
For simple constructs, one-line commands are OK.
BUT ... when things get more complex (with multiple levels of "\" for escaping single-/double-quotes), it might be best to create a job-script, massage that into what is required for the remote host before calling ssh, then have ssh reference that in the following manner:
ssh user#remote 'bash -s' < local_script.sh
As for the line doing the check for failed password, I would never leave that to the end. I would have that as the first line, check the output, and abandon if the password failed, avoiding the likely failed attempts for the other commands, which at that point serves no purpose.

Executing multiple commands with ssh, including evaluation of environment variables on the remote machine

I sometimes use ssh to run multiple commands on a remote host like this:
ssh my_user#my_host "command1; command2"
utilizing double quotes to send both commands to the remote machine.
Now I would like to access an environment variable in one or both of the commands.
Here is a simplified example that illustrates what I have tried:
ssh my_user#my_host "echo $USER; echo $HOSTNAME"
This echos my user/hostname on the local machine, whereas I would like to see the names on the remote machine instead.
Using single quotes I can achieve what I want for a single command as follows:
ssh my_user#my_host echo '$USER'
but I don't get the behavior I want when I use single quotes inside the double quotes like this:
ssh my_user#my_host "echo '$USER'; echo '$HOSTNAME'"
How can I get the behavior I want with both commands being executed from a single ssh commmand?
(Please note that my actual commands are more complicated... I do realize that I can get both variables in a single command for my toy example as below, but this does not solve my real problem):
ssh my_user#my_host echo '$USER' '$HOST'
Just escape the $ using \.
ssh my_user#my_host "echo \$USER; echo \$HOSTNAME"
You can put the entire command to be run in single quotes, like so:
ssh my_user#my_host 'echo $USER; echo $HOSTNAME'
That will prevent your shell from substituting the environment variable values before passing them to the remote machine, but when they get to the remote machine they are unquoted so they get evaluated there.
Thanks to Guilherme Richter. That solution works! I will accept it once the 5 minutes has passed. I wanted to document that in the meantime I found another solution that avoids the double quotes instead of avoiding the single quotes by escaping the semicolon:
ssh user#host echo '$USER' \; echo '$USER'

Bash commands as variables failing when joining to form a single command

ssh="ssh user#host"
dumpstructure="mysqldump --compress --default-character-set=utf8 --no-data --quick -u user -p database"
mysql=$ssh "$dumpstructure"
$mysql | gzip -c9 | cat > db_structure.sql.gz
This is failing on the third line with:
mysqldump --compress --default-character-set=utf8 --no-data --quick -u user -p database: command not found
I've simplified my actualy script for the purpose of debugging this specific error. $ssh and $dumpstructure aren't always being joined together in the real script.
Variables are meant to hold data, not commands. Use a function.
mysql () {
ssh user#host mysqldump --compress --default-character-set=utf8 --nodata --quick -u user -p database
}
mysql | gzip -c9 > db_structure.sql.gz
Arguments to a command can be stored in an array.
# Although mysqldump is the name of a command, it is used here as an
# argument to ssh, indicating the command to run on a remote host
args=(mysqldump --compress --default-character-set=utf8 --nodata --quick -u user -p database)
ssh user#host "${args[#]}" | gzip -c9 > db_structure.sql.gz
Chepner's answer is correct about the best way to do things like this, but the reason you're getting that error is actually even more basic. The line:
mysql=$ssh "$dumpstructure"
doesn't do anything like what you want. Because of the space between $ssh and "$dumpstructure", it'll parse this as environmentvar=value command, which means it should execute the "mysqldump..." part with the environment variable mysql set to ssh user#host. But it's worse than that, since the double-quotes around "$dumpstructure" mean that it won't be split into words, and so the entire string gets treated as the command name (rather than mysqldump being the command name, and the rest being arguments to it).
If this had been the right way to go about building the command, the right way to stick the parts together would be:
mysql="$ssh $dumpstructure"
...so that the whole combined string gets treated as part of the value to assign to mysql. But as I said, you really should use Chepner's approach instead.
Actually, commands in variables should also work and can be in form of `$var` or just $($var). If it says command not found, it could because the command maybe not in you PATH. Or you should give full path of you command.
So let's put this vote down away and talk about this question.
The real problem is mysql=$ssh "$dumpstructure". This means you'll execute $dumpstructure with additional environment mysql=$ssh. So we got command not found exception. It's actually because mysqldump is located on remote server not this host, so it's reasonable this command is not found.
From this point, let's see how to fix this question.
OP want to dumpplicate mysql data from remote server, which means $dumpstructure shoud be executed remotely. Let's see third line mysql=$ssh "$dumpstructure". Now we figure out this would result in problem. So what should be the correct command? The simplest command should be like mysql="$ssh $dumpstructure", which means both $ssh and $dumpstructure will be join into single command line in variable $mysql.
At the end, let's talk about the last command line. I do not agree with variable are meant to hold data, not command. Cause command is also a kind of data. The real problem is how to use it correctly.
OP's command is also supported, at least it is supported on bash 4.2.46.
So the real problem is how to use a variable to hold commands not import a new method to do that, wraping them into a bash function, for example.
So who can tell me why this answer does not come into readers' notice but be voted down?

Whats wrong with this list of servers in my bash script?

I am writing a simple bash script (checkServs.sh) that will ssh into a list of servers and perform a health check on them.
I keep getting errors on the following line:
SERVERS=(blah1.example.com blah2.example.com blah3.example.com blah4.example.com)
Error is:
checkServs.sh: 3: checkServs.sh: Syntax error: "(" unexpected
I've checked online examples and this seems correct, isn't it? Thanks in advance!
I don't know about the syntax error, but this should work:
SERVERS="blah1.example.com blah2.example.com blah3.example.com blah4.example.com"
for server in $SERVERS
do
echo $server
done
EDIT: As noted by Jonathan Leffler in a comment, maybe you are not running the script with bash. Other shells, such as dash, may not recognize the array syntax. If that's the case, you can do:
SERVERS=(blah1.example.com blah2.example.com blah3.example.com blah4.example.com)
for i in $(seq 0 3)
do
echo ${SERVERS[$i]}
done
But if you just want to loop through the names and run an SSH command (ie if having an array won't provide useful functionality), the first method is more straightforward.
Your remote server probably calls a different shell when executing commands. Try to add bash -c to your arguments:
ssh user#server bash -c "<your commands>"
Or:
ssh user#server bash < yourscript.sh ## None in yourscript.sh must read input though.
An opening parenthesis starts a subshell, which is not a correct thing to have on the right side of an equals sign. It expects a string expression, not a command.
Quotation marks are used to keep a string expression together.

Resources