Reading environment variables with ad-hoc ansible command - linux

I am trying to run a simple ad-hoc ansible command on various hosts to find out if a directory exists.
The following command works correctly by returning exists although it does not print the environment variable beforehand:
ansible all -i hosts.list -m shell -a "if test -d /the/dir/; then echo '$HOSTNAME exists'; fi"
Can anyone please tell me why only exists is returned instead of HOSTNAME exists?

Because it's included in double quotes ("), your $HOSTNAME is being interpreted by your local shell. You probably want to write instead:
ansible all -i hosts.list -m shell -a \
'if test -d /the/dir/; then echo "$HOSTNAME exists"; fi'
In most cases you will want to use single quotes (') for your argument to -a when you're using the shell module to prevent your local shell from expanding variables, etc, that are intended to be expanded on the remote host.

Related

How can I write a bash script that sets a variable that's available to the user in the terminal? [duplicate]

This question already has answers here:
Can I export a variable to the environment from a Bash script without sourcing it?
(13 answers)
Closed 3 years ago.
The community reviewed whether to reopen this question last year and left it closed:
Original close reason(s) were not resolved
I'm trying to write a shell script that, when run, will set some environment variables that will stay set in the caller's shell.
setenv FOO foo
in csh/tcsh, or
export FOO=foo
in sh/bash only set it during the script's execution.
I already know that
source myscript
will run the commands of the script rather than launching a new shell, and that can result in setting the "caller's" environment.
But here's the rub:
I want this script to be callable from either bash or csh. In other words, I want users of either shell to be able to run my script and have their shell's environment changed. So 'source' won't work for me, since a user running csh can't source a bash script, and a user running bash can't source a csh script.
Is there any reasonable solution that doesn't involve having to write and maintain TWO versions on the script?
Use the "dot space script" calling syntax. For example, here's how to do it using the full path to a script:
. /path/to/set_env_vars.sh
And here's how to do it if you're in the same directory as the script:
. set_env_vars.sh
These execute the script under the current shell instead of loading another one (which is what would happen if you did ./set_env_vars.sh). Because it runs in the same shell, the environmental variables you set will be available when it exits.
This is the same thing as calling source set_env_vars.sh, but it's shorter to type and might work in some places where source doesn't.
Your shell process has a copy of the parent's environment and no access to the parent process's environment whatsoever. When your shell process terminates any changes you've made to its environment are lost. Sourcing a script file is the most commonly used method for configuring a shell environment, you may just want to bite the bullet and maintain one for each of the two flavors of shell.
You're not going to be able to modify the caller's shell because it's in a different process context. When child processes inherit your shell's variables, they're
inheriting copies themselves.
One thing you can do is to write a script that emits the correct commands for tcsh
or sh based how it's invoked. If you're script is "setit" then do:
ln -s setit setit-sh
and
ln -s setit setit-csh
Now either directly or in an alias, you do this from sh
eval `setit-sh`
or this from csh
eval `setit-csh`
setit uses $0 to determine its output style.
This is reminescent of how people use to get the TERM environment variable set.
The advantage here is that setit is just written in whichever shell you like as in:
#!/bin/bash
arg0=$0
arg0=${arg0##*/}
for nv in \
NAME1=VALUE1 \
NAME2=VALUE2
do
if [ x$arg0 = xsetit-sh ]; then
echo 'export '$nv' ;'
elif [ x$arg0 = xsetit-csh ]; then
echo 'setenv '${nv%%=*}' '${nv##*=}' ;'
fi
done
with the symbolic links given above, and the eval of the backquoted expression, this has the desired result.
To simplify invocation for csh, tcsh, or similar shells:
alias dosetit 'eval `setit-csh`'
or for sh, bash, and the like:
alias dosetit='eval `setit-sh`'
One nice thing about this is that you only have to maintain the list in one place.
In theory you could even stick the list in a file and put cat nvpairfilename between "in" and "do".
This is pretty much how login shell terminal settings used to be done: a script would output statments to be executed in the login shell. An alias would generally be used to make invocation simple, as in "tset vt100". As mentioned in another answer, there is also similar functionality in the INN UseNet news server.
In my .bash_profile I have :
# No Proxy
function noproxy
{
/usr/local/sbin/noproxy #turn off proxy server
unset http_proxy HTTP_PROXY https_proxy HTTPs_PROXY
}
# Proxy
function setproxy
{
sh /usr/local/sbin/proxyon #turn on proxy server
http_proxy=http://127.0.0.1:8118/
HTTP_PROXY=$http_proxy
https_proxy=$http_proxy
HTTPS_PROXY=$https_proxy
export http_proxy https_proxy HTTP_PROXY HTTPS_PROXY
}
So when I want to disable the proxy,
the function(s) run in the login shell and sets the variables
as expected and wanted.
It's "kind of" possible through using gdb and setenv(3), although I have a hard time recommending actually doing this. (Additionally, i.e. the most recent ubuntu won't actually let you do this without telling the kernel to be more permissive about ptrace, and the same may go for other distros as well).
$ cat setfoo
#! /bin/bash
gdb /proc/${PPID}/exe ${PPID} <<END >/dev/null
call setenv("foo", "bar", 0)
END
$ echo $foo
$ ./setfoo
$ echo $foo
bar
This works — it isn't what I'd use, but it 'works'. Let's create a script teredo to set the environment variable TEREDO_WORMS:
#!/bin/ksh
export TEREDO_WORMS=ukelele
exec $SHELL -i
It will be interpreted by the Korn shell, exports the environment variable, and then replaces itself with a new interactive shell.
Before running this script, we have SHELL set in the environment to the C shell, and the environment variable TEREDO_WORMS is not set:
% env | grep SHELL
SHELL=/bin/csh
% env | grep TEREDO
%
When the script is run, you are in a new shell, another interactive C shell, but the environment variable is set:
% teredo
% env | grep TEREDO
TEREDO_WORMS=ukelele
%
When you exit from this shell, the original shell takes over:
% exit
% env | grep TEREDO
%
The environment variable is not set in the original shell's environment. If you use exec teredo to run the command, then the original interactive shell is replaced by the Korn shell that sets the environment, and then that in turn is replaced by a new interactive C shell:
% exec teredo
% env | grep TEREDO
TEREDO_WORMS=ukelele
%
If you type exit (or Control-D), then your shell exits, probably logging you out of that window, or taking you back to the previous level of shell from where the experiments started.
The same mechanism works for Bash or Korn shell. You may find that the prompt after the exit commands appears in funny places.
Note the discussion in the comments. This is not a solution I would recommend, but it does achieve the stated purpose of a single script to set the environment that works with all shells (that accept the -i option to make an interactive shell). You could also add "$#" after the option to relay any other arguments, which might then make the shell usable as a general 'set environment and execute command' tool. You might want to omit the -i if there are other arguments, leading to:
#!/bin/ksh
export TEREDO_WORMS=ukelele
exec $SHELL "${#-'-i'}"
The "${#-'-i'}" bit means 'if the argument list contains at least one argument, use the original argument list; otherwise, substitute -i for the non-existent arguments'.
You should use modules, see http://modules.sourceforge.net/
EDIT: The modules package has not been updated since 2012 but still works ok for the basics. All the new features, bells and whistles happen in lmod this day (which I like it more): https://www.tacc.utexas.edu/research-development/tacc-projects/lmod
Another workaround that I don't see mentioned is to write the variable value to a file.
I ran into a very similar issue where I wanted to be able to run the last set test (instead of all my tests). My first plan was to write one command for setting the env variable TESTCASE, and then have another command that would use this to run the test. Needless to say that I had the same exact issue as you did.
But then I came up with this simple hack:
First command ( testset ):
#!/bin/bash
if [ $# -eq 1 ]
then
echo $1 > ~/.TESTCASE
echo "TESTCASE has been set to: $1"
else
echo "Come again?"
fi
Second command (testrun ):
#!/bin/bash
TESTCASE=$(cat ~/.TESTCASE)
drush test-run $TESTCASE
You can instruct the child process to print its environment variables (by calling "env"), then loop over the printed environment variables in the parent process and call "export" on those variables.
The following code is based on Capturing output of find . -print0 into a bash array
If the parent shell is the bash, you can use
while IFS= read -r -d $'\0' line; do
export "$line"
done < <(bash -s <<< 'export VARNAME=something; env -0')
echo $VARNAME
If the parent shell is the dash, then read does not provide the -d flag and the code gets more complicated
TMPDIR=$(mktemp -d)
mkfifo $TMPDIR/fifo
(bash -s << "EOF"
export VARNAME=something
while IFS= read -r -d $'\0' line; do
echo $(printf '%q' "$line")
done < <(env -0)
EOF
) > $TMPDIR/fifo &
while read -r line; do export "$(eval echo $line)"; done < $TMPDIR/fifo
rm -r $TMPDIR
echo $VARNAME
Under OS X bash you can do the following:
Create the bash script file to unset the variable
#!/bin/bash
unset http_proxy
Make the file executable
sudo chmod 744 unsetvar
Create alias
alias unsetvar='source /your/path/to/the/script/unsetvar'
It should be ready to use so long you have the folder containing your script file appended to the path.
It's not what I would call outstanding, but this also works if you need to call the script from the shell anyway. It's not a good solution, but for a single static environment variable, it works well enough.
1.) Create a script with a condition that exits either 0 (Successful) or 1 (Not successful)
if [[ $foo == "True" ]]; then
exit 0
else
exit 1
2.) Create an alias that is dependent on the exit code.
alias='myscript.sh && export MyVariable'
You call the alias, which calls the script, which evaluates the condition, which is required to exit zero via the '&&' in order to set the environment variable in the parent shell.
This is flotsam, but it can be useful in a pinch.
You can invoke another one Bash with the different bash_profile.
Also, you can create special bash_profile for using in multi-bashprofile environment.
Remember that you can use functions inside of bashprofile, and that functions will be avialable globally.
for example, "function user { export USER_NAME $1 }" can set variable in runtime, for example: user olegchir && env | grep olegchir
Another option is to use "Environment Modules" (http://modules.sourceforge.net/). This unfortunately introduces a third language into the mix. You define the environment with the language of Tcl, but there are a few handy commands for typical modifications (prepend vs. append vs set). You will also need to have environment modules installed. You can then use module load *XXX* to name the environment you want. The module command is basically a fancy alias for the eval mechanism described above by Thomas Kammeyer. The main advantage here is that you can maintain the environment in one language and rely on "Environment Modules" to translate it to sh, ksh, bash, csh, tcsh, zsh, python (?!?!!), etc.
I created a solution using pipes, eval and signal.
parent() {
if [ -z "$G_EVAL_FD" ]; then
die 1 "Rode primeiro parent_setup no processo pai"
fi
if [ $(ppid) = "$$" ]; then
"$#"
else
kill -SIGUSR1 $$
echo "$#">&$G_EVAL_FD
fi
}
parent_setup() {
G_EVAL_FD=99
tempfile=$(mktemp -u)
mkfifo "$tempfile"
eval "exec $G_EVAL_FD<>'$tempfile'"
rm -f "$tempfile"
trap "read CMD <&$G_EVAL_FD; eval \"\$CMD\"" USR1
}
parent_setup #on parent shell context
( A=1 ); echo $A # prints nothing
( parent A=1 ); echo $A # prints 1
It might work with any command.
I don't see any answer documenting how to work around this problem with cooperating processes. A common pattern with things like ssh-agent is to have the child process print an expression which the parent can eval.
bash$ eval $(shh-agent)
For example, ssh-agent has options to select Csh or Bourne-compatible output syntax.
bash$ ssh-agent
SSH2_AUTH_SOCK=/tmp/ssh-era/ssh2-10690-agent; export SSH2_AUTH_SOCK;
SSH2_AGENT_PID=10691; export SSH2_AGENT_PID;
echo Agent pid 10691;
(This causes the agent to start running, but doesn't allow you to actually use it, unless you now copy-paste this output to your shell prompt.) Compare:
bash$ ssh-agent -c
setenv SSH2_AUTH_SOCK /tmp/ssh-era/ssh2-10751-agent;
setenv SSH2_AGENT_PID 10752;
echo Agent pid 10752;
(As you can see, csh and tcsh uses setenv to set varibles.)
Your own program can do this, too.
bash$ foo=$(makefoo)
Your makefoo script would simply calculate and print the value, and let the caller do whatever they want with it -- assigning it to a variable is a common use case, but probably not something you want to hard-code into the tool which produces the value.
Technically, that is correct -- only 'eval' doesn't fork another shell. However, from the point of view of the application you're trying to run in the modified environment, the difference is nil: the child inherits the environment of its parent, so the (modified) environment is conveyed to all descending processes.
Ipso facto, the changed environment variable 'sticks' -- as long as you are running under the parent program/shell.
If it is absolutely necessary for the environment variable to remain after the parent (Perl or shell) has exited, it is necessary for the parent shell to do the heavy lifting. One method I've seen in the documentation is for the current script to spawn an executable file with the necessary 'export' language, and then trick the parent shell into executing it -- always being cognizant of the fact that you need to preface the command with 'source' if you're trying to leave a non-volatile version of the modified environment behind. A Kluge at best.
The second method is to modify the script that initiates the shell environment (.bashrc or whatever) to contain the modified parameter. This can be dangerous -- if you hose up the initialization script it may make your shell unavailable the next time it tries to launch. There are plenty of tools for modifying the current shell; by affixing the necessary tweaks to the 'launcher' you effectively push those changes forward as well.
Generally not a good idea; if you only need the environment changes for a particular application suite, you'll have to go back and return the shell launch script to its pristine state (using vi or whatever) afterwards.
In short, there are no good (and easy) methods. Presumably this was made difficult to ensure the security of the system was not irrevocably compromised.
The short answer is no, you cannot alter the environment of the parent process, but it seems like what you want is an environment with custom environment variables and the shell that the user has chosen.
So why not simply something like
#!/usr/bin/env bash
FOO=foo $SHELL
Then when you are done with the environment, just exit.
You could always use aliases
alias your_env='source ~/scripts/your_env.sh'
I did this many years ago. If I rememeber correctly, I included an alias in each of .bashrc and .cshrc, with parameters, aliasing the respective forms of setting the environment to a common form.
Then the script that you will source in any of the two shells has a command with that last form, that is suitable aliased in each shell.
If I find the concrete aliases, I will post them.
Other than writings conditionals depending on what $SHELL/$TERM is set to, no. What's wrong with using Perl? It's pretty ubiquitous (I can't think of a single UNIX variant that doesn't have it), and it'll spare you the trouble.

Having trouble with running a command over ssh

This command isn't working as expected:
ssh root#<machineIP> -- sh -c "echo \$\(cat /tmp/testfile\) > /testfile"
My intent is to copy the contents of /tmp/testfile to /testfile. Real simple. But I find that /testfile has nothing in it. The file is created (in the case it doesn't exist).
echo command works fine (minus the escapes) if run from command line on the remote server. But doesn't work when running it through ssh. Originally I actually had a more complex command with 'sed', but simplified what wasn't working down to this.
Both remote and local servers are CentOS7.
I found I had to escape the $ and (). Is this causing me problems? Is not the correct way to run this command?
ssh will take all the command parameters, join them on spaces and then run that in the remote shell.
That means that the command being executed is basically what you'd get if you replaced ssh with echo:
$ echo sh -c "echo \$(cat /tmp/testfile) > /testfile"
sh -c echo $(cat /tmp/testfile) > /testfile
The quoting is lost, so the resulting command is equivalent to sh -c echo > /testfile which makes it empty.
Instead, just take the command you want to run and wrap it in single quotes:
ssh root#host 'echo $(cat /tmp/testfile) > /testfile'
Make sure never to use echo or $() when copying files though. I'm assuming this is a stand-in for something better.

Use the same command in shell script, just with a prefix

I have a command I run to check if a certain db exists.
I want to do it locally and via ssh on a remote server.
The command is as so:
mysqlshow -uroot | grep -o $DB_NAME
My question is if I can use the same command for 2 variables,
the only difference being ssh <remote-server> before one?
Something along the lines of !! variable expansion in the CLI:
LOCAL_DB=mysqlshow -uroot | grep -o $DB_NAME
REMOTE_DB=ssh <remote-host> !!
something like this perhaps?
cmd="whoami"
eval $cmd
ssh remote#host $cmd
eval will run the command in the string $cmd locally
also, for checking tables, it's safer to ask for the table name explicitly via a query
SHOW TABLES LIKE 'yourtable';
and for databases:
SHOW DATABASES LIKE 'yourdb';
You can create a function in .bashrc something like:
function showrdb() {
ssh remote#host "$1"
}
export -f showrdb
and then source .bashrc and call the function like;
showrdb "command you want to run on remote host"
Or alternately you can create a shell script contains the same function(or only the ssh line) and call the script as
./scriptname "command to execute of remote host"
But the level of comfort for me is more in first approach.

How to echo line with multiple quotes/special characters into file?

I am trying to echo the following line into a .profile but it keeps getting confused by either the many quotes or special characters.
bind '"e[A": history-search-backward'
I've tried all sorts of things but can't get it nailed.
This is what I currently have:
sudo su -c 'echo "bind \'\"\\e[A\": history-search-backward\'" >> /etc/profile' -
This is what it returns:
su: user '"\e[A": does not exist
Yet if I just use:
echo bind \'\"\\e[A\": history-search-backward\'" >> /home/user/testfile
It works just fine.
I have all manner of "sudo su -c "echo blah..." in the rest of my script that work just fine.
Any ideas?
Try this
sudo su -c $'echo \"bind \'\"\\e[A\": history-search-backward\'\" >> /etc/profile\' -'
From the bash man page:
A single quote may not occur between single quotes, even when preceded by a backslash.
Text quoted by $'...' may contain backslash-escaped quotes, both single and double.
Another option is to add a simpler expression to ~/.inputrc:
echo '"\e[A": history-search-backward' >> ~/.inputrc
There doesn't seem to be a system-wide equivalent of .inputrc that is read by all users. Also, this makes the key binding available to any program that uses readline. If you really do want to restrict it to bash, add a conditional expression:
cat >> ~/.inputrc <<'EOF'
$if Bash
"\e[A": history-search-backward
$endif
EOF
Every character is interpreted literally between single quotes, except ' itself. So you can put a single quote inside a literal string like this: 'single'\''quoted' is the string single'quoted.
Your command is complicated because there are two shells involved: the shell you're running this command from, and the shell that su runs. Note that it's weird to run sudo su since sudo already runs the specified command as root; sudo sh -c … makes more sense. So you need to quote for both. It's usually clearest to use single quotes for the outer shell, and double quotes or single quotes or backslashes for the inner shell.
There's another problem with your command: you're targeting the wrong file. /etc/profile is only read by login shells, whereas the bind command is specific to bash but should be read by all instances of bash, not just login shells. Instead of writing this line to /etc/profile, you should write it to the system-wide bashrc, if there is one (it's usually /etc/bash.bashrc).
sudo sh -c 'echo "bind '\''\"\\e[A\": history-search-backward'\''" >>/etc/bash.bashrc'
You may put this setting directly into the readline configuration file, /etc/inputrc. You'll save on a level of quoting.
sudo sh -c 'echo '\''"\e[A": history-search-backward'\'' >>/etc/inputrc'
An easier way to pass an arbitrary string to a command would be to pass it as input instead of as an argument and use a here document.
sudo sh -c 'cat >>/etc/inputrc' <<'EOF'
"\e[A": history-search-backward
EOF

Pass variables to remote script through SSH

I am running scripts on a remote server from a local server via SSH. The script gets copied over using SCP in a first place, then called while being passed some arguments as follows:
scp /path/to/script server.example.org:/another/path/
ssh server.example.org \
MYVAR1=1 \
MYVAR2=2 \
/another/path/script
This works fine and on the remote server, the variables MYVAR1 and MYVAR2 are available with their corresponding value.
The issue is that these scripts are in constant development which requires the SSH command to be changed every-time a variable is renamed, added, or removed.
I'm looking for a way of passing all the local environment variables to the remote script (since MYVAR1 and MYVAR2 are actually local environment variables) which would address the SSH command maintenance issue.
Since MYVAR1=1 \ and MYVAR1=1 \ are lines which follow the env command output I tried replacing them with the actual command as follows:
ssh server.example.org \
`env`
/another/path/script
This seems to work for "simple" env output lines (e.g. SHELL=/bin/bash or LOGNAME=sysadmin), however I get errors for more "complex" output lines (e.g. LS_COLORS=rs=0:di=01;34:ln=01;[...] which gives errors such as -bash: 34:ln=01: command not found ). I can get rid of these errors by unsetting the variables corresponding to those complex output lines before running the SSH command (e.g. unset LS_COLORS, then ssh [...]) however I don't find this very solution very reliable.
Q: Does anybody know how to pass all the local environment variables to a remote script via SSH?
PS: the local environment variables are not environment variables available on the remote machine so I cannot use this solution.
Update with solution
I ended using sed to format the env command output from VAR=VALUE to VAR="VALUE" (and concatenating all lines in to 1) which prevents bash from interpreting some of the output as commands and fixes my problem.
ssh server.example.org \
`env | sed 's/\([^=]*\)=\(.*\)/\1="\2"/' | tr '\n' ' '` \
"/another/path/script"
I happened to read the sshd_config man page unrelated to this and found the option AcceptEnv:
AcceptEnv
Specifies what environment variables sent by the client
will be
copied into the session's environ(7). See SendEnv in
ssh_config(5) for how to configure the client. Note that
envi-
ronment passing is only supported for protocol 2.
Variables are
specified by name, which may contain the wildcard
characters *'
and?'. Multiple environment variables may be separated
by
whitespace or spread across multiple AcceptEnv directives.
Be
warned that some environment variables could be used to
bypass
restricted user environments. For this reason, care
should be
taken in the use of this directive. The default is not to
accept
any environment variables.
Maybe you could use this with AcceptEnv: *? I haven't got a box with sshd handy, but try it out!
The problem is that ; mark the end of your command. You must escape them:
Try whit this command:
env | sed 's/;/\\;/g'
Update:
I tested the command whit a remote host and it worked for me using this command:
var1='something;using semicolons;'
ssh hostname "`env | sed 's/;/\\\\;/g' | sed 's/.*/set &\;/g'` echo \"$var1\""
I double escape ; whit \\\\; and then I use an other sed substitution to output variables in the form of set name=value;. Doing this ensure every variables get setted correclty on the remote host before executing the command.
You should use set instead of env.
From the bash manual:
Without options, the name and value of each shell variable are displayed in a format that can be reused as input for setting or resetting the currently-set variables.
This will take care of all your semi-colon and backslash issues.
scp /path/to/script server.example.org:/another/path/
set > environment
scp environment server.example.org:/another/path/
ssh server.example.org "source environment; /another/path/script"
If there are any variables you don't want to send over you can filter them out with something like:
set | grep -v "DONT_NEED" > environment
You could also update the ~/.bash_profile on the remote system to run the environment script as you log in so you wouldn't have to run the environment script explicit:
ssh server.example.org "/another/path/script"
How about uploading the environment at the same time?
scp /path/to/script server.example.org:/another/path/
env > environment
scp environment server.example.org:/another/path
ssh server.example.org "source environment; /another/path/script"
Perl to the rescue:
#!/usr/bin/perl
use strict;
use warnings;
use Net::OpenSSH;
use Getopt::Long;
my $usage = "Usage:\n $0 --env=FOO --env=BAR ... [user\#]host command args\n\n";
my #envs;
GetOptions("env=s" => \#envs)
or die $usage;
my $host = shift #ARGV;
die $usage unless defined $host and #ARGV;
my $ssh = Net::OpenSSH->new($host);
$ssh->error and die "Unable to connect to remote host: " . $ssh->error;
my #cmds;
for my $env (#envs) {
next unless defined $ENV{$env};
push #cmds, "export " . $ssh->shell_quote($env) .'='.$ssh->shell_quote($ENV{$env})
}
my $cmd = join('&&', #cmds, '('. join(' ', #ARGV) .')');
warn "remote command: $cmd\n";
$ssh->system($cmd);
And it will not break in case your environment variables contain funny things as quotes.
This solution works well for me.
Suppose you have script which takes two params or have two variables:
#!/bin/sh
echo "local"
echo "$1"
echo "$2"
/usr/bin/ssh root#192.168.1.2 "/path/test.sh \"$1\" \"$2\";"
And script test.sh on 192.168.1.2:
#!/bin/bash
echo "remote"
echo "$1"
echo "$2"
Output will be:
local
This is first params
And this is second
remote
This is first params
And this is second

Resources