The Shell: Automation
Volume Number: 21 (2005)
Issue Number: 4
Column Tag: Programming
Mac In The Shell
by Edward Marczak
The Shell: Automation
Doing Something Useful
Automation: it's everything. There's little other reason to have computers in a business. While that may be a slight oversimplification, let's agree that it's a compelling reason to have them around. Sometimes, you automate smaller tasks so you can free up your time to automate the bigger tasks.
Introduction
Last month, we touched on some basic shell commands. This month, we'll take those, and a new one or two, and create a script that helps us automate some system maintenance.
Processing...
The March "Mac In The Shell" talked about how the bash shell works with files, interprets pattern matching characters, and can get you help. As you've been practicing your shell-fu, hopefully you've found some new commands that are simply too useful to give up. For now, though, they're just commands. By chaining commands together, or letting them interact in any way, they become greater than the sum of their parts. Unix is built on this philosophy. You'll find that most commands are small, single-purpose programs. Each program can concentrate on being great at one thing. Then, we can write a script or function, or use i/o redirection to have the output of one program act as input to another.
Here's an example: 'wc' is the "word, line, character, and byte count" program. You can feed it a file, and it will happily spit back these statistics:
$ wc smallscript.sh
93 299 2350 smallscript.sh
'smallscript.sh' has 93 lines, 299 words and 2350 bytes. Sometimes, you just have to know these things. Additionally, 'wc' accepts switches that tailor the output to your needs. If you just need to know how many lines, use the '-l' (ell) switch:
$ wc -l smallscript.sh
93 smallscript.sh
But what if we need to know how many lines in a directory listing? The 'ls' command doesn't have an option for that. That's why we let each program do what it does best! We'll let the 'ls' command list the directory, and have 'wc' use the output from 'ls' as its input. An example from my home directory:
$ ls -l | wc -l
44
(Yes, that's about 20-or-so too many, OK?) The "|" character is the pipe, or more properly, "pipeline." I'll give a quick overview of the going-ons here, but for more information and usage examples, see my article in the December 2004 MacTech.
Most of us take for granted the fact that we have a keyboard in front of us, and when we type, characters show up on the screen. Additionally, few people run Terminal.app and remember that this is supposed to be emulating a real terminal. Well, in some ways, the underlying system does still treat each terminal session as if it were a real, physical terminal hooked into it. It does this through the use of virtual terminals (or, 'pseudo-terminals'). I suppose "Virtual Terminal.app" just didn't roll off the tongue.
Whether you're using a real text-terminal connected via wire to an AIX server, or Terminal.app locally on your OS X box, some things are constant. We all have learned (or should have, at some point), about "Input, Process, Output" in computer information handling. When you're running a shell, there are defaults for 'input' and 'output', called 'standard input' and 'standard output'. Standard input is taken from your typing at the keyboard. Standard output is the screen. There are several ways to redirect this input and output, and the pipe is one of the ways to do that.
In the previous example, we could certainly have done this:
$ ls -l > filelist.txt
$ wc -l filelist.txt
The '>' character lets us redirect standard output. Instead of the output from 'ls' printing to the screen as it usually does, we redirect it to a file. Then we can have 'wc' operate on the file. However, it's a lot easier and neater to use the pipe redirector to do it all in one step, isn't it?
The Automator
Apple apparently agrees with my stance on automation, as evidenced by the 'Automator' feature of Tiger. However, in reality, you are the automator. We're not quite up to the stage where operating systems and applications really watch what you do over time, and then modify behavior purely on your actions. So, you have to automate actions. Automator in Tiger is a great step in this direction, and it will even call shell scripts for you!
A fair amount of automation is already built into OS X for you. Shell scripts that clean up log files, rebuild databases, and more run from cron. 'cron' is a system utility that runs shell scripts ("jobs," more precisely, as it can really kick off any program, such as perl, PHP or python) on a set schedule. We learned how to massage cron and have it run our own jobs from Josh Wisenbaker in the March issue of MacTech. Despite the maintenance that already exists in OS X, there's always more work to do. Here's an example of a script that I like to run on OS X Server, but there are parts that will be useful on OS X as well (remember that OS X makes a great server, too, as it will run pretty much anything that OS X Server will). I'll also introduce one of my favorite commands.
What We Need To Know
An OS X Server box that is tasked with just about everything that it can do has a lot of responsibilities: DNS, e-mail, file serving, web serving, and more. There are certain critical parameters that we should be kept abreast of, such as free disk space, if there's anything stuck in the mail queue, if any binaries have been altered, etc. I like knowing this summary every morning. So, how can we get this in a simple, single source? A shell script that e-mails its output to us, of course!
First, running out of disk space pretty universally bad. OS X, with its "I advocate a single partition" disk scheme makes watching disk space even more critical. 'df' is the tool we use to find out how much disk space is used and free on local, mounted volumes. I like using the '-h' 'human readable' switch:
$ df -h
Filesystem Size Used Avail Capacity Mounted on
/dev/disk4 173G 73G 100G 42% /
devfs 110K 110K 0B 100% /dev
fdesc 1.0K 1.0K 0B 100% /dev
<volfs> 512K 512K 0B 100% /.vol
/dev/disk5 234G 221G 13G 95% /Volumes/Data2
automount -nsl [436] 0B 0B 0B 100% /Network
automount -fstab [449] 0B 0B 0B 100% /automount/Servers
automount -static [449] 0B 0B 0B 100% /automount/static
/dev/disk10 932G 444G 487G 48% /Volumes/Data1
It's good to know about all of our volumes, but the super-important one is our system volume. The system volume is represented by the "/" on the first line of output above. We can see that of the three real volumes on this system, one is getting pretty close to being full (Data2 at 95% capacity = 'only' 13GB left!).
We can find out about what's in the mail queue with the 'mailq' command. This will tell us what has been deferred in a sendmail or postfix queue. I just happen to have an example: (see bottom figure.) Here, we see this server has three messages that are still waiting to be delivered for one reason or another. Sometimes, you'll find that messages to a certain destination are backed up because the mail host is genuinely down or unreachable. Other times, you'll find that routing is borked on your end, and the queued messages have a bit more of a random pattern.
There are several systems that will take a hash of your files, and then later tell you if anything has changed. This has become known as a tripwire function, named so after the application "Tripwire" that basically fathered this genre of utility. Naturally, the goal of running a system like this is security. If someone/something replaces your 'ls' binary with one that hides certain files, you want to know about it. I run tripwire on most of my Mac OS X boxes, more as a notification of things that have been installed than for the paranoid security reasons, so that's what I'll discuss here. Of course, there are others, such as radmind (which also can do a lot more than simply compare files), and Checkmate. We'll be discussing tripwire in a future column, but for now, I'm just going to go though the motions.
First, I go dump all of the old reports from the reports directory:
rm -f /usr/local/tripwire/report/*
Then, we want to run the tripwire and generate our new report:
/usr/local/tripwire/bin/tripwire -m c
Finally, we can update the database with the new changes, so we're reset for the next run:
/usr/local/tripwire/bin/tripwire -m u -a -r
/usr/local/tripwire/report/`ls /usr/local/tripwire/report`
-P "My really long passphrase" -v
What's a system without backup? Even if I have a tape backup running, I like to a) copy certain files locally on the same
$ mailq
-Queue ID- -Size- -Arrival Time- -Sender/Recipient-
0C4125B83A8 2615 Fri Apr 1 02:59:56 MAILER-DAEMON
(Name service error for name=example.com type=MX: Host not found, try again)
Clarence.Smith@example.com
B8E2A4EF0EF 2628 Thu Mar 31 01:28:52 MAILER-DAEMON
(Name service error for name=example.com type=MX: Host not found, try again)
Eileen.Doe@example.com
D7ACD4439EE 3204 Wed Mar 30 07:59:25 MAILER-DAEMON
(connect to mail.example.com[10.0.0.112]: Operation timed out)
bob@example.com
-- 9 Kbytes in 3 Requests.
disk, simply for ease of access, and b) get files off of the server to a remote location. I use rsync for both of these tasks. If you plan on implementing this as outlined here, please note: you must replace the Apple-supplied rsync with RsyncX if you want to sync files with resource forks (http://archive.macosxlabs.org/rsyncx/rsyncx.html). So, what do we want to copy here? I like to get /etc locally:
rsync -a -q --delete /private/etc/ /backup/etc/
On a server running mail, I like to grab the mail store:
rsync -a -q --delete /var/imap/ /backup/var/imap/
rsync -a -q --delete /var/spool/imap/ /backup/var/spool/imap/
Lastly, I like to move certain files off-site. rsync will do that, too:
rsync --delete -l -a -v /Shares/shared/ store.radiotope.com:
/Volumes/Backup/rsync-bu/shared/ --eahfs -stats
Lastly, I make sure I clean up any directories that may be building up files. Some of my servers run OpenBase databases (for the excellent DayLite by MarketCircle). One of the options for OpenBase is an automatic backup. Great! The only problem with this is that, while it's happy to make the backup, it doesn't know when to get rid of it. Files can pile up pretty quickly, which means disk space can disappear quickly, too. Here's where my favorite command comes in. The 'find' command is one of those commands that certainly fits the philosophy that we discussed earlier. However, it's also a bit of a Swiss Army knife in that it will handle certain tasks even though other utilities would do the job just fine. Yes, this will be talked about in more depth next month. The find command lets us find files based on several different criteria. One parameter is the age of the file. I want to delete any OpenBase backup files that haven't been modified for 7 days:
find /Library/OpenBase/Backup/ -mtime +7 -delete
This will rid us of files that have not been modified in 7 days in the /Library/OpenBase/Backup directory. find is very powerful, and can be a system admin's best friend on many occasions. However, while you're learning, please note: find is also one of those commands that you have to be a little careful with, and it will descend recursively into all of the directories below the path you specify. Used with the "-delete" switch, it can do damage! As a last measure in my old-file-removal routine, I like to also dump old quarantined mail caught by amavis:
find /var/quarantine/ -mtime +7 -delete
Put It All Together
Now that we know exactly what we want to do, let's turn it into a shell script. To do this, we need a text editor. I typically reach for vi. Apple tends to lead people through these examples with pico. While there are some GUI tools that will also do the trick (TextWrangler and SubEthaEdit come to mind), this is a column on using the terminal, and that's what we'll do! Remember: vi, emacs and pico can easily be used over ssh, with no access to a GUI at all - TextWrangler and SubEthaEdit can't!
Creating a shell script involves a few steps. We'll call this one 'nightlymaint.sh'. Get into vi to edit this file by typing "vi nightlymaint.sh". Press the 'i' key to go into insert mode. You should now see the word 'INSERT' at the bottom of your screen. First thing we need to do is to declare what kind of script this is:
#!/bin/bash
Alternatively, this could have been a perl script (#!/usr/bin/perl), PHP, Python, a different shell such as tcsh, or other variety. When you invoke this as an executable, the shell looks at this first line to determine which interpreter to use to run your script. This is a nice shorthand way to execute a script, rather than have to specify everything on the command-line. Then, we'll add each of our commands, one by one, with some comments. So, the whole thing looks like this:
#!/bin/bash
# nightlymaint.sh
# Our nightly maintenance routine
# Problems? Contact Joe Admin
# version 1.0 - 2005-04-04
# Let the logs know that we've begun
logger -p local0.notice -i -t Nightly Starting nightly routine
# Get disk stats
/usr/bin/df -h
# See what's in the mail queue
/usr/bin/mailq
# Run tripwire reports
logger -p local0.notice -i -t Running tripwire reports
rm -f /usr/local/tripwire/report/*
/usr/local/tripwire/bin/tripwire -m c
/usr/local/tripwire/bin/tripwire -m u -a -r /usr/local/tripwire/report/`ls
/usr/local/tripwire/report` -P "My really long passphrase" -v
# Backup files
logger -p local0.notice -i -t Backing up files
/usr/bin/rsync -a -q --delete /private/etc/ /backup/etc/
/usr/bin/rsync -a -q --delete /var/imap/ /backup/var/imap/
/usr/bin/rsync -a -q --delete /var/spool/imap/ /backup/var/spool/imap/
/usr/bin/rsync --delete -l -a -v /Shares/shared/ store.radiotope.com:
/Volumes/Backup/rsync-bu/shared/ --eahfs -stats
# Clean up old files
find /Library/OpenBase/Backup/ -mtime +7 -delete
find /var/quarantine/ -mtime +7 -delete
logger -p local0.notice -i -t Nightly maintenance complete
Lines that start with a hash mark ('number sign') are comments and are ignored by the bash interpreter. This script will execute, one line at a time from the beginning to the end. It sends its output to standard output. However, we're going to run this from cron, which, by default, will take any standard output and e-mail it to the owner of the crontab.
Let's save this script. If you're still with me in vi, press the escape key once, and then type ":" (colon, not semi-colon - so hit the shift key, too). The colon character puts you into command mode. When you see the colon character at the bottom of your screen, type 'wq' and press enter. That's short for 'w'rite the file and then 'q'uit. You'll be back at the command line. So, just one more thing: we need to mark this script executable. Type "chmod u+x nightlymaint.sh" and press return. This adds the executable bit to the permissions of our script. Without it, the shell won't (easily) execute our file.
I want to test this script, but a word of warning first: Do NOT run this script as-is. It needs to be adjusted for your environment. The 'logger', 'du' and 'mailq' lines are harmless. Everything else needs to be changed to some sane value. You may or may not have tripwire installed. If you do not, simply remove that section. What directories are you concerned with backing up? Naturally, the 'Clean up old files' section will remove files! Make sure you either remove these lines, or point them at a directory that makes sense for you!
Did you read all that? Really? OK. Now we'll....wait. Go read the preceding paragraph again....I'll wait.......OK, thanks. If you're really ready, let's run this sucker. At the command line, type "./nightlymaint.sh" (don't type the quotes), press enter, and you'll see a good deal of info come spewing back at you. You can also check /var/log/system.log and you'll see lines like the following produced by our script:
Apr 4 18:28:13 localhost Nightly[668]: maintenance complete
The output you see on your screen is the same information that will get sent to you via e-mail.
Last comments on this script: you may have noticed that I prefixed each command with its full path in the script, but usually don't bother when I simply call it on the command-line. Two reasons: First, we want to be sure we get the right binary, if there are multiple on our system. Secondly, it's a bit of a (good) habit: depending on how your script gets called, it may not have access to the environment, which means no path variable. So, absolute paths should go everywhere in a script like this.
Additionally, if you ever need to put a password or passphrase into a shell script (rare, but it happens), you should mark the file only readable by root ("chown root nightlymaint.sh", followed by "chmod 700 nightlymaint.sh").
Schedule It
We don't want to have to run this manually every day, do we? (Shouts of, "No!" heard nearby) So, let's get this into cron.
A script like this will need to run privileged as root. There are a few ways to do this. We could drop it in root's crontab. We could drop a line in the system crontab. Or, as a variant, we could ignore messing with cron directly at all. They're all good methods, and which you choose really depends on your style. However, OS X already schedules maintenance tasks that run on daily, weekly and monthly schedules. We're going to take advantage of that.
We need our script to run on a daily basis, so let's move our script to the appropriate place:
sudo mv nightlymaint.sh /etc/periodic/daily/700.nightlymaint.sh
sudo chmod 550 /etc/periodic/daily/700.nightlymaint.sh
sudo chown root:wheel /etc/periodic/daily/700.nightlymaint.sh
That's all! Now, the system will run our script as part of its regular 3:15am daily maintenance.
Closing time
This month we learned how to create a simple shell script. This script runs through its commands in order, with no conditions or loops. I will say that this exercise glossed over a few small details, and completely ignored the fact that OS X Server already has a decent way to monitor free disk space (called "diskspacemonitor"), and do some of what we talked about (see /var/log/daily.out). This example script certainly isn't perfect, nor was it meant to be all-inclusive of every system administration ill. It introduced you to a few new commands. More importantly, it should give you ideas. It should allow you to create something that works for you.
You can now start to automate some tasks that are repetitive in your computing environment. Perhaps you need stats on processes chewing up too much CPU. Perhaps you need to monitor open file count. Whatever it is, now you have a way. Ask questions - here if you'd like. You can send in questions to emarczak@mactech.com Once you start automating, you start getting some time back. Time to read that new book on OS X, time to spend with friends and family, time to watch that new Battlestar Galactica episode, or time to rest. All fine goals, no?
Ed Marczak owns and operates Radiotope, a technology consulting company that asks, "how can we help you automate that?" Automate at http://www.radiotope.com