SSH Create Directory In Remote Site Using Perl Script - linux

Previously I have asked a question here on how to determine whether a path is a directory or not in remote site using SSH. I wish to create the directory if the path is not a directory. I have tried following code with two ways but it seem not to be working. Thanks for everyone that helps here.
use File::Path;
my $destination_path = "<path>";
my $ssh = "usr/bin/ssh";
my $user_id = getpwuid( $< );
my $site = "<site_name>";
my $host = "rsync.$site.com";
if (system("$ssh $user_id\#$host [ -d $destination_path ]") == 0) {
print "It is a directory.\n";
} else {
print "It is not a directory.\n";
#First Way
if(system("$ssh $user_id\#$host [ make_path ($d_path_full) ]") == 0{
#Second Way
if(system("$ssh $user_id\#$host [ mkdir -p $d_path_full ]") == 0{
print "Create directory successfully.\n";
} else {
print "Create directory fail.\n";
}
}

The bracket(s), single [ or the pair [ ], is a builtin in bash which is a test operator (see man test), and the last use of it is incorrect. But you don't need it to make a directory
use warnings;
use strict;
use feature 'say';
my $ssh = '/usr/bin/ssh';
my $user_id = ...
my $host = ...
my $to = quotemeta $user_id.'#'.$host;
my $cmd = 'mkdir -p TEST_MKDIR_OVER_SSH';
system("$ssh $to $cmd") == 0 or die "Can't mkdir: $!";
The mkdir is quiet with -p if a directory already exists, and it returns succes what also defeats the purpose of [ ] (if that was the intent). But an actual error -- a file with that name exists, no permissions on the path, etc -- does make its way back to the script, as you'd want, and a string with the error message is in $! so please test for this.
If you simply wish to know whether the directory existed please put back your test branch, or just omit -p and analyze the $! for what that message is on your system.
As for the second attempt: the command to be executed runs on the remote system and has nothing to do with this script anymore (apart from interpolated variables). So Perl functions or libraries from this script make no sense in that command.
For the next step I suggest to look into modules for (preparing and) running external commands, that are much more helpful than the bare system.
Some, from simple to more capable: IPC::System::Simple, Capture::Tiny, IPC::Run3, IPC::Run. Also see String::ShellQuote, to prepare commands and avoid quoting issues, shell injection bugs, and other problems. This recent post is a good example, and there's a lot more out there.

I would recommend using a proper module to do SSH, namely Net::OpenSSH, a SSH client built upon OpenSSH.
While being implemented in pure Perl, it is fast and stable, and has no mandatory dependency (apart of course, OpenSSH binaries).
As explained in the docs, it will, under certain conditions, automatically quote any shell metacharacters in the command lists.
The following codes demonstrates how it can respond to your use case. It relies on the same shortcut explained by #zdim, using mkdir -p :
if the directory does not exists, it gets created (if that fails, an error happends)
if it already exists, nothing happens
if a file exists with the target name, an error happens
Code :
use warnings;
use strict;
use Net::OpenSSH;
my $host = ...;
my $user_id = ...;
my $destination_path = ...;
# connect
my $ssh = Net::OpenSSH->new($host, user => $user_id);
$ssh->error and die "Can't ssh to $host: " . $ssh->error;
# try to create the directory
if ( $ssh->system('mkdir', '-p', $destination_path) ) {
print "dir created !\n";
} else {
print "can't mkdir $dir on $host : " . $ssh->error . "\n";
}
# disconnect
undef $ssh;

Related

Pass a indicator from Bash back to Perl over SSH via STDIN

We have a Linux server which can run a diagnostic script, diag.pl, which coordinates reporting over other servers.
diag.pl iterates over the child servers, and for each of them, SSHs in and runs a bash script, which passes information back:
my $cmd=sprintf("ssh %s sudo /usr/lib/support/report.sh -e %s | uudecode -o \"%s-outfile.tgz\") 2>%1 |", $server, $specialparam, $servername)
The line of code in report.sh that sends the data back is:
uuencode --base64 ${REPORT}.tar.gz /dev/stdout
I would like to update report.sh to send back an additional line of information, something like:
echo "special-file-found=${SFF}" > /tmp/sff.cfg
uuencode --base64 /tmp/sff.cfg > /dev/stdout
Once the special file has been found, the Perl script will update so that it no longer sends the specialparam back to subsequent report.sh calls.
Is there a good way to send that input so that it will be easy for Perl to catch it?
What have I tried
Setting a user.comment attr on the tar.gz using setattr, but the comment does not survive the uuencoding
Currently thinking that my best bet is to use the pseudocode above, creating a new file to encode and send along, and update the Perl script to check it with each new transmission until it finds the special file.
I take it that the objective is to modify a shell script which returns to the caller an encoded file, so that it sends yet more information, specifically a string to be used as a flag in the caller.
It is not clear how the shell script is run from the Perl script, but there are ways to do this so that the caller gets back separate "lines" that are printed, either as they are emitted or altogether after the run completes.
Then you can just add to the shell script the needed extra print to STDOUT, and in the caller check each line of shell output to see whether it conforms to some "protocol;" for example, whether it is, or starts with, special-file-found string. Then you can set flags for further calls or write control file for following runs, etc. Otherwise, the line is the encoded file.
A made-up basic example using pipe-open (see by the end of the page)
use warnings;
use strict;
use feature 'say';
my #cmd = qw(ls -l ./);
my $file_found = quotemeta 'special-file-found';
my ($flag, $binfile);
my $pid = open(my $out, '-|', #cmd) // die "Can't open #cmd: $!";
while (<$out>) {
chomp;
if (/^$file_found/) {
$flag = 1;
}
else {
$binfile = $_;
# whatever else need be done, or perhaps last;
}
}
close $out;
This example runs the command ls -l ./ but instead of it you can run any executable, like #cmd = ('report.sh', 'arg1', 'arg2',...).
Another way is to use backticks (qx) and assign its return to an array, in which case each element receives a line of output.
Yet another, better, way is to use a module which manages external commands. For example, from simple to more capable: IPC::System::Simple, Capture::Tiny, IPC::Run3, IPC::Run.

Dual use bash script - source but also exec subshell? Dynamic return/exit?

My current setup starts with a function that is ostensibly in .bashrc (.bash_it/custom/funcs.bash to be precise)
#!/usr/bin/env bash
function proset() {
. proset-core "$#";
}
proset-core does some decrypting of secrets and exports those secrets to the session, hence the need for the . instead of just running it as a script/subshell.
If something goes wrong in proset-core, I use return instead of exit since I don't want the SSH connection to be dropped.
if [ "${APP_JSON}" = "null" ] ; then
echo -e "\n${redtext}App named $NAME not found in ${APPCONF}. Aborting.${resettext}\n";
return;
fi
This makes sense in the context of the exported proset function, but precludes usage as a script since return isn't valid except from within a function.
Is there a way to detect how it's being called and return one or the other as appropriate?
Just try to return, and exit if it fails.
_retval=$?
return 2>/dev/null || exit "$_retval"
The only case where your code will still be continuing after the return was invoked at top-level (outside of a function) is if you were executed rather than sourced, and should that happen, exiting is the Right Thing.
Make the builtin variable $SHLVL part of $# args as the last arg. Then at test point:
if [ "${#: -1}" -lt $SHLVL ]; then
# SHLVL arg is less than current SHLVL
# we are in a subshell
exit
else
return
fi
Ended up using
calledBy="$(ps -o comm= $PPID)";
if [ "x${calledBy}" = "xsshd" ]; then
return 1;
else
exit 1;
fi
since it didn't require passing anything extra. Anything that might cause this to be problematic please comment. Not too worried about being bash-specific or portable.
Credit: get the name of the caller script in bash script

Installation script in Perl not functioning correctly

I have a program that gets installed using the following Perl script. The installation does not work and I get the message"No installer found." Obviously, nothing was done as the script just simply dies.
Here is the Perl install script (it is for installing a program called Simics):
#!/usr/bin/perl
use strict;
use warnings;
# Find the most recent installer in the current working directory.
my $installer;
my $highest_build = 0;
opendir my $d, "." or die $!;
foreach (readdir $d) {
if (-f && -x && /^build-(\d+)-installer/) {
if ($1 > $highest_build) {
$highest_build = $1;
$installer = $_;
}
}
}
closedir $d;
die "No installers found.\n" unless defined $installer;
exec "./$installer", #ARGV;
Stepping through your code above, this line:
foreach (readdir $d) {
reads the name of each of the files in the directory you opened to the handle "$d" and assigns each of those files in turn to the thing variable ($). (This variable is one of those weird but brilliant Perl idiosyncrasies. You don't have to mention $ in most cases; it's just there.)
Then in the next line:
if (-f && -x && /^build-(\d+)-installer/) {
The "-f" and the "-x" are file test operators. Since neither one has an explicit argument (e.g., -f "myfile.txt") they will use the implied thing variable, $_. The -f operator just checks to see if something is a file and the -x checks to see if the file is executable, (as indicated by the executable bit being set.) The third part, /^build-(\d+)-installer/, checks to see if it matches that pattern.
As you mentioned in your comment above, the directory listing shows
-rw------- 1 nikk nikk 52238 Feb 27 20:50 build-4607-installer.pl
The rw------- shows the file permissions for each of three groups, the owner ("nikk") and the group that owns the file (second "nikk"). The first three characters, starting with rw-, show that nikk can read and write from the file - but not execute. The listing would show rwx if nikk could execute the file. The next two groups of three characters, --- and --- show that neither the group nikk nor anyone else on the machine can read, write, or execute.
More information on Unix file system permissions
The lack of execute permission is causing the "-x" test to fail. There are two ways of fixing this. Either remove the -x from the if test so that it looks like this:
if (-f && /^build-(\d+)-installer/) {
Or add execute permission to the file. To do that just for the owner (assuming your program is running as user nikk or as root, do this:
chmod u+x build-4607-installer.pl
More information on chmod.
I hope that's helpful!

File existence in expect commands

I am new in using 'expect' commands, my requirement is to create an expect script to check if a file exists in a specific location.
The script i am using is
#!/usr/bin/expect
set PATH .ssh
set fname id_rsa
set timeout 2
spawn -noecho ls $PATH
expect { "id_rsa" { puts "found" }
timeout { puts "not found" } }
But this script not working. Is there any approach available for this requirement like File.Exists ($fname) ? I need to decide next action based on the file existence. like if file exits, then do nothing, else create a rsa key pair, but stuck at this point.
Any help would be appreciated.
Thanks
In Tcl/expect:
if {[file exists [file join $PATH id_rsa]]} {
puts found
} else {
puts "not found"
}
Also, your PATH variable is dependant on your current directory. You should use
set PATH [file join $env(HOME) .ssh]
Also, when shell scripting, never use the variable PATH -- you won't be able to locate any programs if you redefine PATH. In general, don't use ALL_CAPS_VARNAMES, leave those for the system.
You can use if condition in bash script for checking file is present or not.
if [ -e $PATH/id_rsa ]; then
echo "found"
else
echo "not found"
fi

Shell prompt that is based on location in filesystem

I have to work within three main directories under the root filesystem - home/username, project, and scratch. I want my shell prompt to display which of these top level directories i am in.
Here is what I am trying to do:
top_level_dir ()
{
if [[ "${PWD}" == *home* ]]
then
echo "home";
elif [[ "${PWD}" == *scratch* ]]
then
echo "scratch";
elif [[ "${PWD}" == *project* ]]
then
echo "project";
fi
}
Then, I export PS1 as:
export PS1='$(top_level_dir) : '
Unfortunately this is not working as I want. I get home : for my prompt when I am in my home directory, but if I switch to scratch or projects then the prompt does not change. I do not understand bash scripting very well so I would appreciate any help to correct my code.
You can hook into cd to change the prompt every time you are changing the working directory. I've asked myself often how to hook into cd but I think that I now found a solution. What about adding this to your ~/.bashrc?:
#
# Wrapper function that is called if cd is invoked
# by the current shell
#
function cd {
# call builtin cd. change to the new directory
builtin cd $#
# call a hook function that can use the new working directory
# to decide what to do
color_prompt
}
#
# Changes the color of the prompt depending
# on the current working directory
#
function color_prompt {
pwd=$(pwd)
if [[ "$pwd/" =~ ^/home/ ]] ; then
PS1='\[\033[01;32m\]\u#\h:\w\[\033[00m\]\$ '
elif [[ "$pwd/" =~ ^/etc/ ]] ; then
PS1='\[\033[01;34m\]\u#\h:\w\[\033[00m\]\$ '
elif [[ "$pwd/" =~ ^/tmp/ ]] ; then
PS1='\[\033[01;33m\]\u#\h:\w\[\033[00m\]\$ '
else
PS1='\u#\h:\w\\$ '
fi
export PS1
}
# checking directory and setting prompt on shell startup
color_prompt
Please try this method instead and tell us how it works e.g. how your prompt changes in your home directory, your project or scratch directory, and other directories besides those. Tell us what error messages you see as well. The problem lies within it.
Tell me also how you run it, if it's by script, by direct execution, or through a startup script like ~/.bashrc.
top_level_dir ()
{
__DIR=$PWD
case "$__DIR" in
*home*)
echo home
;;
*scratch*)
echo scratch
;;
*project*)
echo project
;;
*)
echo "$__DIR"
;;
esac
}
export PS1='$(top_level_dir) : '
export -f top_level_dir
If it doesn't work, try changing __DIR=$PWD to __DIR=$(pwd) and tell us if it helps too. I also would like to confirm if you're really running bash. Note that there are many variants of sh like bash, zsh, ksh, and dash and the one installed and used by default depends on every system. To confirm that you're using Bash, do echo "$BASH_VERSION" and see if it shows a message.
You should also make sure that you're running export PS1='$(top_level_dir) : ' with single quotes and not with double quotes: export PS1="$(top_level_dir) : ".

Resources