Before I can actually test my script, I have some setup to do. So, I want to make 2
sure that I have /etc/hosts entries for the servers that I'm going to be calling 3
by name, and one way we can do this is with echo... 4
and combine that with sudo tee -a /etc/hosts. 5
Okay, that looks good. 6
Let's see if I can ping them by name. Okay, 10.9.8.11 for server01, 7
and I can ping server02 as well when that corresponds to that 10.9.8.12 8
address, so that looks good. So now, the next thing I need to do is configure 9
SSH authentication, if I haven't. And this is a new server - I just created it from 10
scratch, as you saw just a couple of minutes ago - so i need to create a key 11
pair on my admin01 server. So, I'll do that with ssh-keygen. I'm just going 12
to accept the default options here. I'm not going to use a passphrase because I want 13
to execute commands on remote systems without having to enter a passphrase. Now 14
I need to copy that public key to our servers, so I'm going to copy it to 15
server01. Yes, I'm going to trust that key, and then I'm going to type in 16
"vagrant" for the password. I'm going to do the same thing for 17
server02 - "yes", and the password is "vagrant". So now I should be able to 18
execute a command on one of those systems without a password, so if I do 19
ssh server01 and i execute the hostname command - 20
whoops, if I type the hostname correctly - then it will execute on the remote system 21
and return its host name, which is server01. Do the same thing with server02. 22
Connects remotely, executes command, exits, and we're back to admin01. So, this 23
is looking good. So here I am in my /vagrant folder, and I want to create this 24
list of default servers that we coded for in our script. So I'm just going to 25
do this: server01, then add server02. Now, the single greater-than sign 26
truncates or overrides a file, so if that servers file existed it would have been 27
zeroed out and then the contents of the echo command would have been stored 28
in that file. With the double greater-than signs, it appends to 29
whatever's in that file. So here we have server01 and server02 in our 30
file. Now we're ready to test our script. Of course. the first time we execute our 31
script, we want to make sure it has the proper permissions. 32
Let's test to see if our check about whether or not it was run with root 33
privileges works. So, we'll run it with sudo. It says, "Do not execute this script 34
as root. Use the -s option instead," and it gives us our usage statement, and of 35
course it should exit with a non zero exit status, which it does. So, that looks 36
like it's working perfectly. Now, let's make sure the script displays a usage 37
message if we don't supply a command to execute on the list of servers. So, if we 38
run this command without any options or any commands, it should show us that 39
usage statement. And it does, so that looks good. 40
Now, let's try our script with an invalid option. Let's do this: run-everywhere.sh -x, 41
and we'll say the "hostname" command is what we want to execute everywhere, but 42
it turns out -x is an invalid option, and getopts catches that, and our script 43
looks good. Okay, so this time let's execute this script the way it's supposed to be 44
executed. So, we'll give the name of our script and the command we want executed 45
on all the remote systems. So, sure enough, it executes the hostname command on 46
server01, which returns of course server01. and the hostname command on 47
server02, which returns server02. Now let's execute the script using the dry 48
run option, which I opted to use the -n for. By the way, I use that because it's 49
a familiar option to me with some other Linux commands - for example, rsync has 50
-n for dry run, there are some other examples - so you could have probably used 51
-d if you wanted to, but it seems to be a convention, at least, to use -n for a 52
dry run option. Anyway, enough of that aside, let's try it out in our script. 53
So, it would have executed ssh -o "ConnectTimeout=2" server01 54
and then the "hostname" command. Now, you may not notice it here, 55
but there is actually an additional space here, and that's because we have a 56
space here, and then the $SUDO variable (for which we didn't use -s, so it's not set, so 57
it doesn't get displayed), and then a space after that $SUDO variable. So we end 58
up with two spaces here. So that's how that's going to work, by the way. 59
Okay, let's use the -v option, so we'll do this: run-everywhere.sh -v. Let's use the 60
"uptime" command that displays how long a system has been up, and it's load average, 61
and how many users are logged in, and that sort of thing - pretty simple command, 62
can be kind of interesting. Let's see what happens. Okay, server01 has been 63
up 42 minutes, server02 has been up 41 minutes, and load average is zero because 64
nothing's going on on those systems. Let's try to combine some options like 65
the -n and -s options. So, -n is a dry run, -s is for sudo. So "-ns id", and the 66
command that would have been executed would have been that ssh with the ssh 67
options, the server name, and then the SUDO variable, which evaluates to "sudo", 68
and then the command. So, again, here we had two spaces, here we have a space, and 69
then the contents of that SUDO variable, and another space. So, again, that's how 70
that is going to work. This time, let's run the "id" command with -v. If we didn't 71
use the -v option in this particular case the output is the same, so we 72
wouldn't have known what servers the output was coming from. 73
So using this -v option can help with that, so that's kind of why we coded it 74
in here in our little script. Let's create our own little mini server list 75
here. We'll just put server01 into that list, and we'll just call this list test, 76
cat test, okay, it only has one server in there, and then let's go ahead and use 77
that list and execute a command on that list. 78
So, again, just one server in the list, it looped over that list - again, just one 79
doesn't make a lot of sense, but if you have multiple servers then you can use 80
different lists if you want - and then it executed the "hostname" command that we 81
specified. So, that looks good. Now, let's test the case where we provide some 82
invalid data. So, let's say, run-everywhere.sh to a file that doesn't exist. Okay, it looks 83
like our check worked: Can't open the server list file /path/to/nowhere - 84
of course, that file is made-up and fake and doesn't 85
exist, so again our script is working as it should. Now, let's quickly create a 86
"test1" account on all the servers in our server list, and of course creating 87
accounts requires superuser or root privileges. Okay, so we'll run our command with 88
the superuser privileges, we'll run the useradd command, and we'll pass it the name 89
of the account we want to create, which is "test1". So the useradd command 90
doesn't generate any output unless it encounters an error, and it looks like no 91
errors were displayed, so how do we know if this worked? Well, one way we can figure 92
it out is by running a command everywhere, and that command will be "id" 93
against the username "test1" to see if it was created. So, sure enough, we get the 94
exact same output from both of those systems because the account was created 95
exactly the same on both of those systems. Now, let's say you want to create 96
a "test2" account with a comment of "Test Two" on all the servers. Because we want 97
to use a quoted string on the remote system - because we're going to quote "Test Two" 98
for our comment - we have to put that command in quotes. And to quote a quote, 99
use the opposing quotation mark. Now, let me just demonstrate that to you on 100
the command line. So, if we do this: echo double quote, single quote, 101
Test Two, single quote, double quote, what we get displayed is, single quote, Test Two, 102
single quote. So, we're using the opposite quotation mark if we want single 103
quotations to be displayed while we encapsulate those in double quotation 104
marks, and if we want to do the opposite, Test Two here with double quotation 105
marks, we encapsulate those with single quotation marks. All right, hopefully that 106
makes some sense. So, if we want to preserve our quotes for the argument to 107
the -c option to the useradd command, we need to make sure that they are 108
quoted. So how we would do this is this: run-everywhere.sh - let's do a dry run to see 109
what would get executed, so we can test to make sure we have our quotes right on 110
the command line before we blast this command on every server and potentially 111
have an account created that doesn't have the right information, or we 112
accidentally create the wrong account and so on. So, let's do the dry run option. 113
We need sudo privileges, or we want to use sudo privileges here. 114
Okay, so what would have gotten executed would be ssh with our SSH options, the server name, 115
then "sudo useradd -c", and sure enough, our comment is enclosed in quotation 116
marks, just like we need it on the remote system. So, this is going to work for us. 117
So now, what we need to do is just go back up here, eliminate our dry run 118
option, and let it execute as root on those remote systems. Now, let's see if it 119
got created. And sure enough, it did, and let's run this with the -v option, and you 120
see exactly what server is generating this output. So sure enough, server01 says 121
we have a test2 account with an ID of 1002, 122
as does server02. We could also do this: we could run-everywhere.sh, 123
-v option to display our server name, and then we'll just tail the last two 124
entries in the /etc/passwd file, and we should see our two accounts that we 125
created: a test1 account that we did not supply a comment for, and a test2 126
account for which we had to be careful about our quotation marks. Now, let's test 127
our script even further by executing a command that doesn't exist, and we want 128
to make sure that our command exits with a non-zero exit status. So let's 129
run-everywhere.sh - I hope this command doesn't exist on your server. If it does, 130
well, I'll be darned. Okay, sure enough, "i-like-eggs" is not a 131
command on server01 or server02, and let's look at our return status. Sure 132
enough, our script exited with a nonzero exit status, so this is exactly what we 133
want to do. Now, here's another test we can do. Let's say one of our servers is 134
down, and what's going to happen when we try to execute a command to that server? 135
Well, let's go ahead and exit out of admin01. Now I'm back to my local 136
system, be it Mac, Windows, or Linux. Then I'm going to halt server02. By the way, 137
if you run "vagrant halt" without specifying a server, it's going to halt 138
all the servers, or all the virtual machines, defined in your Vagrantfile. I 139
want to limit this command to just one server, that's why I specified it here on 140
the command line. Likewise, with ssh, I need to specify what 141
server I'm going to. I'm going to get into admin01. Let's go back to our 142
shared vagrant folder, and this time let's do run-everywhere.sh -v and use the 143
"uptime" command. So, server01 has been up 51 minutes, and server02, the ssh timed 144
out - remember we set the time out to be 2 seconds here. And let's check the exit 145
status of our script. So, it exits with a nonzero exit status. So, if there is a 146
server along the way that you can't connect to, then you get a nonzero exit 147
status with your scripts and you know, "Hey, I need to look at that output and 148
make sure to go and check out that server." Well, that brings us to the end of 149
this exercise. I hope you enjoyed it! I'll be curious to see what you come up with, 150
and of course I hope you find a way to put these concepts and techniques 151
into use into your workflow.