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

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.

Related

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.

How to run command in background and also capture the output

In the middle of the script, I have a command that exposes the local port with ssh -R 80:localhost:8080 localhost.run I need to execute this command in the background, parse the output and save it into a variable.
The output returns:
Welcome to localhost.run!
...
abc.lhrtunnel.link tunneled with tls termination, https://abc.lhrtunnel.link
Need to capture this part:
https://abc.lhrtunnel.link
As a result something like this:
...
hostname=$(command)
echo $hostname
...
Try this Shellcheck-clean code:
#! /bin/bash -p
hostname=$(ssh ... \
| sed -n 's/^.*tunneled with tls termination, //p')
declare -p hostname
I'm assuming that you don't really want to background the command that generates the output. You just want to run it in a way that allows its output to be captured and filtered. See How do you run multiple programs in parallel from a bash script? for information about how "background" processes are used for parallel processing.
The -n option to sed means that it doesn't print lines from the input unless explicitly instructed to print.
s/^.*tunneled with tls termination, //p works on input lines that contain anything followed by the string tunneled with tls termination, . It deletes everything on the line up to the end of that string and prints the result, which hopefully will be the URL that you want.
declare -p varname is a much more reliable and useful way to show the value of a variable than using echo.

How to escape and store shell command line arguments into one argument?

Inb4 anyone saying this is a bad idea, it's actually a reasonable approach for things like this.
I'm writing this Docker container that can execute user provided commands through contained OpenVPN connection in Docker, e.g. docker run vpntunnel curl example.com.
So the ENTRYPOINT of the image will fire up OpenVPN, after the VPN tunnel is up, execute the user provided CMD line.
Problem is, the standard way to run commands after OpenVPN is up is through the --up option of OpenVPN. Here is the man page description of this option:
--up cmd
Run command cmd after successful TUN/TAP device open (pre --user UID change).
cmd consists of a path to script (or executable program), optionally followed
by arguments. The path and arguments may be single- or double-quoted and/or
escaped using a backslash, and should be separated by one or more spaces.
So the reasonable approach here is for ENTRYPOINT script to correctly escape the user provided CMD line and pass the whole thing as one parameter to --up option of OpenVPN.
In case my Docker image needs to perform some initializations after the tunnel is up and before the user command line is executed, I can prepend a script before the user provided CMD line like this: --up 'tunnel-up.sh CMD...' and in the last line of tunnel-up.sh use "$#" to execute user provided arguments.
Now as you may guess, the only problem left is how to correctly escape an entire command line to be able to passed as a single argument.
The naive approach is just --up "tunnel-up.sh $#" but it surely can't distinguish command lines between a b c and "a b" c.
In bash 4.4+ you can use parameter transformation with # to quote values:
--up "tunnel-up.sh ${*#Q}"
In prior versions you could use printf '%q' to achieve the same effect:
--up "tunnel-up.sh $((($#)) && printf '%q ' "$#")"
(The (($#)) check makes sure there are parameters to print before calling printf.)

How can I start a subscript within a perpetually running bash script after a specific string has been printed in the terminal output?

Specifics:
I'm trying to build a bash script which needs to do a couple of things.
Firstly, it needs to run a third party script that I cannot manipulate. This script will build a project and then start a node server which outputs data to the terminal continually. This process needs to continue indefinitely so I can't have any exit codes.
Secondly, I need to wait for a specific line of output from the first script, namely 'Started your app.'.
Once that line has been output to the terminal, I need to launch a separate set of commands, either from another subscript or from an if or while block, which will change a few lines of code in the project that was built by the first script to resolve some dependencies for a later step.
So, how can I capture the output of the first subscript and use that to run another set of commands when a particular line is output to the terminal, all while allowing the first script to run in the terminal, and without using timers and without creating a huge file from the output of subscript1 as it will run indefinitely?
Pseudo-code:
#!/usr/bin/env bash
# This script needs to stay running & will output to the terminal (at some point)
# a string that we need to wait/watch for to launch subscript2
sh subscript1
# This can't run until subscript1 has output a particular string to the terminal
# This could be another script, or an if or while block
sh subscript2
I have been beating my head against my desk for hours trying to get this to work. Any help would be appreciated!
I think this is a bad idea — much better to have subscript1 changed to be automation-friendly — but in theory you can write:
sh subscript1 \
| {
while IFS= read -r line ; do
printf '%s\n' "$line"
if [[ "$line" = 'Started your app.' ]] ; then
sh subscript2 &
break
fi
done
cat
}

Send complex command via ssh

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.

Resources