Would you like to work with me? We are hiring engineers at SemaphoreCI. Apply here.

Running scripts on remote machines

@igor_sarcevic ·

Several days ago a colleague and I wanted to check whether our caching system contains all the files that we expect it to contain. We started by writing a bash command that would list and compare the available files. It had the following structure:

cd somewhere && untar archive && diff <(ls -lah directory) <(ls -lah other_directory)

We run the above command through an ssh connection from our local machine. In other words we did something along these lines:

ssh cacher@cache-server "cd somewhere && untar archive && diff <(ls -lah directory) <(ls -lah other_directory)"

As you can see the above command is pretty long even in this form. But it wasn’t sufficient. We added a couple of sed, grep, awk, tail, while commands just to make the output more human friendly. The result was a 4 lines long beast command that we tried to keep in a one long readline session. Imagine something like this:

ssh cacher@cache-server "cd somewhere && untar archive && ls directory | while read directory; do && diff echo $directory; <(ls -lah directory | grep "*.*\1" | sed "s/\.spec//g" | tail) <(ls -lah other_directory | grep "*.*\1" | sed "s/\.spec//g" | tail) && echo "No errors" || echo "Error!!!"; done"

To make the command even more horrible, we run it on several remote machines with a nifty for loop:

for server in "${servers[@]}"; { ssh cacher@$server "cd somewhere && untar archive && ls directory | while read directory; do && diff echo $directory; <(ls -lah directory | grep "*.*\1" | sed "s/\.spec//g" | tail) <(ls -lah other_directory | grep "*.*\1" | sed "s/\.spec//g" | tail) && echo "No errors" || echo "Error!!!"; done" }

Hunting down syntax errors was tedious, every time moving around with the left and right arrow… It was the time to put the command into a shell script!

The script

When you put the above command in a shell script, it becomes nice and tidy:

cd somewhere
untar archive
ls directory | while read directory; do
  echo $directory;
  original_list=$(ls -lah $directory | grep "*.*\1" | sed "s/\.spec//g" | tail)
  new_list=$(ls -lah other_directory | grep "*.*\1" | sed "s/\.spec//g" | tail)
  if diff <(echo original_list) <(echo new_list); then
    echo "No errors"
  else
    echo "Error!!!";
  fi
done

But, how can we run the script on a remote machine?

The srtaight forward solution is to scp it to the remote machine and then execute the script with an ssh commmand:

for server in "${servers[@]}"; { scp script.sh $server:/tmp/script.sh && ssh cacher@$server "bash /tmp/script.sh && rm /tmp/script.sh"}

But, this still looks horible… Let’s look at an alternative…

Turns out you can push a file into the ssh command and ssh will redirect it to a remote command:

ssh cacher@cache-server "bash -s" < script.sh

With this we remove the need for a temp file, and the above command becomes:

for s in "${servers[@]}"; { ssh cacher@$s "bash -s" < script.sh }

This form looks acceptable.