Exercise: Scripting Remote Commands - Part II
Start course
3h 7m

In this course, we will explore how to set up a network of virtual machines and how to implement SSH key authentication and execute commands on remote systems. We'll look at how to install and remove software from local and remote systems. You'll learn about continue and break statement and their benefits and use cases. You learn how to automate processes on Linux through the use of cron jobs and examine running processes.

This course is part of the Linux Shell Scripting learning path. To follow along with this course, you can download all the necessary resources here.

Learning Objectives

  • Learn how to create a network of virtual machines and how to configure SSH key authentication and execute commands on remote systems via SSH
  • Learn how to install and remove software packages both on your local system as well as on remote systems
  • Understand continue and break statements in loops and what they're used for
  • Understand what cron is and how to use it to schedule the running of scripts in Linux at various intervals
  • Learn how to examine running processes on a Linux system and how to determine their process IDs

Intended Audience

  • Anyone who wants to learn Linux shell scripting 
  • Linux system administrators, developers, or programmers


To get the most out of this course, you should have a basic understanding of the Linux command line.


Before I can actually test my script, I have some setup to do. So I wanna make sure that I have /etc/hosts entries for the servers that I'm going to be calling by name and one way we can do this is with echo and combine that with sudo tee -a /etc/hosts. Okay. That looks good. Let's see if I can ping them by name. Okay. 10, nine, eight, 11, server01. And I can ping server02 as well and that corresponds to that 10, nine, eight, 12 address. So that looks good. So now the next thing I need to do is configure SSH authentication. If I haven't and this is a new server, I just created it from scratch as you saw just a couple of minutes ago so I need to create a key pair on my admin01 server. So I'll do that with this ssh-keygen, I'm just going to accept the default options here. Not gonna use a passphrase because I want to execute commands on remote systems without having to enter a passphrase. Now I need to copy that public key to our servers. So I'm gonna copy it to server01. Yes, I'm gonna trust that key and then I'm going to type in Vagrant for the password, going to do the same thing for Server02. Yes. And the password is Vagrant. So now I should be able to execute a command on one of those systems without a password. So if I do ssh server01 and I execute the host name command, whoops. If I type the host name correctly, then it will execute on the remote system and return its host name which is server01, do the same thing with server02, connects remotely executes command, exits and we're back to admin01. So this is looking good. So here I am in my /vagrant folder, and I wanna create this list of default servers that we coded for in our script. So I'm just gonna do this server01, and add server02. Now the single greater than sign truncates or overwrites the file. So if that service fall existed, it would have been zeroed out and then the contents of the echo command would have been stored in that file with the double greater than signs it appends to whatever's in that file. So here we have server01 and server02 in our file. Now we're ready to test our script. Of course, the first time we execute our script, we want to make sure it has the proper permissions. Let's test to see if our check about whether or not it was run with root privileges work. So we'll run it with sudo. It says, "Do not execute this script as root. use the -s option instead." And it gives us our usage statement and of course it should exit with a non-zero exit status, which it does. So that looks like it's working perfectly. Now, let's make sure the script displays a usage message if we don't supply a command to execute on the list of servers. So if we run this command without any options or any commands, it should show us that usage statement. And it does so that looks good. Now let's try our script with an invalid option. Let's do this. ./ -x and we'll say the host name command is what we want to execute everywhere but it turns out -x is an invalid option and get ups catches that and our script looks good. Okay. So this time let's execute this script the way it's supposed to be executed. So we'll give the name of our script and the command we want executed on all the remote systems. So sure enough, it executes the host name command on server01 which returns of course server01 and the host name command on server02 which returned server02. Now let's execute this script using the dry run option which I opted to use the dash in four, by the way I use that because it's a familiar option to me with some other Linux commands. For example, our has -n for dry run there are some other examples. So you could have probably used -d if you wanted to but it seems to be a convention at least to use -n for a dry run option. Anyway, enough of that aside, let's try it out in our script. So it would have executed ssh -o ConnectTimeout=2 server01 and then the host name command. Now you may not notice it here but there are actually an additional space here. And that's because we have a space here and then sudo variable, which we didn't use to SSH so it's not set. So it doesn't get displayed. And then a space after that sudo variable. So we end up with two spaces here. So that's how that's going to work by the way. Okay. Let's use the -v option. So we'll do this ./ -v. Let's use the uptime command that displays how long a system has been up and its load average and how many users are logged in it and that sort of thing. Pretty simple command can be kinda interesting. Let's see what happens. Okay. Server01 has been up 42 minutes, server02 has been up 41 minutes and load average is zero because nothing's going on on those systems. Let's try to combine some options like the -n and -s option. So in this are dry run sh for sudo. So -ns id and the command that would have been executed would have been that SSH with the SSH options, the server name, and then the sudo variable which evaluates the sudo and then the command. So again here we had two spaces, here we have a space and then the contents of that sudo variable and another space. So again, that's how that is going to work. This time, let's run the ID command with -v. If we didn't use the -v option in this particular case, the output is the same. So we wouldn't have known what servers the output was coming from. So using this -v option can help with that. So that's kinda why we coded it in here in our little script. Let's create our own little mini server list here. We'll just put server01 into that list and we'll just call this list, cat test. Okay. It only has one server in there. And then let's go ahead and use that list and execute a command on that list. So again, just one server in the list it looped over that list. Again, just one doesn't make a lot of sense but if you have multiple servers and you can use different lists if you want and then it executed the hosting command that we specified. So that looks good. Now let's test the case where we provide some invalid data. So let's say ./ to a file that doesn't exist. Okay. It looks like our check worked. Can't open the server list file, /path/to/nowhere. Of course, that file is made up and fake and doesn't exist. So again, our script is working as it should. Now let's quickly create a test1 account on all the servers in our server list and of course creating accounts require super user or root privileges. Okay. So we'll run our command with super user privileges. We'll run the useradd command and we'll pass it the name of the account we want to create which is test1. So the user app command doesn't generate any output unless it encounters an error and it looks like no errors were displayed. So how do we know if this works? Well, one way we can figure it out is by running a command everywhere and that command will be ID against the username test1 to see if it was created. So sure enough, we get the exact same output from both of those systems because the account was created exactly the same on both of those systems. Now, let's say you want to create a test2 account with a comment of test2 on all the servers because we want to use a quoted string on the remote system because we're going to quote test2 for our comment. We have to put that command in quotes and to quote a quote, use the opposing quotation mark. And now let me just demonstrate that to you on the command line. So if we do this echo "'Test Two'" what we get displayed is 'Test Two'. So we're using the opposite quotation mark if we want single quotations to be displayed while we encapsulate those in double quotation marks and if we want to do the opposite, '"Tests Two"' here with double quotation marks we encapsulate those with single quotation marks. All right, hopefully that makes some sense. So if we want to preserve our quotes for the argument to the -c option to the user add command, we need to make sure that they are quoted. So how we would do this is this ./ Let's do a dry run to see what would get executed so we can test to make sure we have our quotes right on the command line before we blast this command on every server and potentially have an account created that doesn't have the right information or we accidentally create the wrong account and so on. So let's do the dry run option. We need sudo privileges or we wanna use sudo privileges here. Okay. So what would have gotten executed would be SSH with our SSH options, the server name then sudo useradd -c and sure enough our comment is enclosed in quotation marks just like we need it on the remote system. So this is going to work for us. So now what we need to do is just go back up here, eliminate our dry run option and let it execute as root on those remote systems. Now let's see if it got created and sure enough, it did. And let's run this with -v option and you see exactly what server is generating this output. So sure enough server01 says we have a test2 account with an ID of 1002 as does server02. We could also do this. We could ./ -v option to display our sudo name and then we'll just tell the last two entries in the /etc/password file. And you should see our two accounts that we created a test1 account that we did not supply a comment for and a test2 account for which we had to be careful about our quotation marks. Now let's test our script even further by executing a command that doesn't exist. And we wanna make sure that our command exits with a non-zero exit status. So let's ./ a command... I hope this command doesn't exist on your server. If it does well, I'll be darned. Hey, sure enough. I like eggs. It's not a command on server01 or server02. And let's look at our return status. Sure enough, our script exited with a non-zero exit status. So this is exactly what we want to do. Now here's another test we can do. Let's say one of our servers is down and what's gonna happen when we try to execute a command to that server. Well, let's go ahead and exit out of admin01. Now I'm back to my local system be it Mac, windows or Linux. Then I'm going to halt server02. By the way, if you ran Vagrant halt without specifying a server, it's going to halt all the servers or all the virtual machines defined in your Vagrant file. I wanna limit this command to just one server, that's why I specified it here on the command line. Likewise, with SSH I need to specify what server I'm going to, I'm gonna get into admin01, let's go back to our shared Vagrant folder and this time let's do ./ -v and use the uptime command. So server01 has been up 51 minutes and server02 the SSH timed out. Remember we set the time out to be two seconds here and let's check the exit status of our script. So it exits with a non-zero exit status. So if there's a server along the way that you can't connect to, then you get a non-zero exit status with your script. And you know, "Hey, I need to look at that output and make sure to go and check out that server." Well, that brings us to the end of this exercise, I hope you enjoyed it. I'll be curious to see what you come up with and of course, I hope you can find a way to put these concepts and techniques into use into your workflow.

About the Author
Learning Paths

Jason is the founder of the Linux Training Academy as well as the author of "Linux for Beginners" and "Command Line Kung Fu." He has over 20 years of professional Linux experience, having worked for industry leaders such as Hewlett-Packard, Xerox, UPS, FireEye, and Nothing gives him more satisfaction than knowing he has helped thousands of IT professionals level up their careers through his many books and courses.

Covered Topics