No sophisticated operating system, whether it’s Windows, OS X, or Unix, can run indefinitely without human intervention. If you’re on a multiuser Linux system, someone is already performing the necessary system administration tasks. You might be able to ignore the proverbial “man behind the curtain” who is managing and maintaining everything, or you might well be the Great and Powerful Oz yourself, the person who pulls the levers and pushes the buttons to keep the system running. If you have a single-user system, there are system administration tasks that you should be performing on a regular basis.
Fortunately, simplifying life for Linux system administrators (the goal for this chapter) is one of the most common uses of shell scripting. In fact, quite a few Linux commands are actually shell scripts, and many of the most basic tasks, like adding users, analyzing disk usage, and managing the filespace of the guest account, can be accomplished more efficiently with short scripts.
What’s surprising is that many system administration scripts are no more than 20 to 30 lines long. Heck, you can use Linux commands to identify scripts and run a pipe to figure out how many lines each contains. Here are the 15 shortest scripts in /usr/bin/:
$ file /usr/bin/* | grep "shell script" | cut -d: -f1 | xargs wc -l \ | sort -n | head -15 3 zcmp 3 zegrep 3 zfgrep 4 mkfontdir 5 pydoc 7 sgmlwhich 8 batch 8 ps2pdf12 8 ps2pdf13 8 ps2pdf14 8 timed-read 9 timed-run 10 c89 10 c99 10 neqn
None of the shortest 15 scripts in the /usr/bin/ directory are longer than 10 lines. And at 10 lines, the equation-formatting script neqn is a fine example of how a little shell script can really improve the user experience:
#!/bin/bash # Provision of this shell script should not be taken to imply that use of # GNU eqn with groff -Tascii|-Tlatin1|-Tutf8|-Tcp1047 is supported. : ${GROFF_BIN_PATH=/usr/bin} PATH=$GROFF_BIN_PATH:$PATH export PATH exec eqn -Tascii ${1+"$@"} # eof
Like neqn, the scripts presented in this chapter are short and useful, offering a range of administrative capabilities including easy system backups; the creation, management, and deletion of users and their data; an easy-to-use frontend for the date command that changes the current date and time; and a helpful tool to validate crontab files.
Even with the advent of very large disks and their continual drop in price, system administrators seem to be perpetually tasked with keeping an eye on disk usage so that shared drives don’t fill up.
The most common monitoring technique is to look at the /usr or /home directory, using the du command to determine the disk usage of all subdirectories and reporting the top 5 or 10 users. The problem with this approach, however, is that it doesn’t take into account space usage elsewhere on the hard disk(s). If some users have additional archive space on a second drive, or you have some sneaky types who keep MPEGs in a dot directory in /tmp or in an unused directory in the ftp area, this usage will escape detection. Also, if you have home directories spread across multiple drives, searching each /home isn’t necessarily optimal.
Instead, a better solution is to get all the account names directly from the /etc/passwd file and then to search the filesystems for files owned by each account, as shown in Listing 5-1.
#!/bin/bash # fquota--Disk quota analysis tool for Unix; assumes all user # accounts are >= UID 100 MAXDISKUSAGE=20000 # In megabytes for name in $(cut -d: -f1,3 /etc/passwd | awk -F: '$2 > 99 {print $1}') do /bin/echo -n "User $name exceeds disk quota. Disk usage is: " # You might need to modify the following list of directories to match # the layout of your disk. The most likely change is from /Users to /home. ➊ find / /usr /var /Users -xdev -user $name -type f -ls | \ awk '{ sum += $7 } END { print sum / (1024*1024) " Mbytes" }' ➋ done | awk "\$9 > $MAXDISKUSAGE { print \$0 }" exit 0
Listing 5-1: The fquota script
By convention, user IDs 1 through 99 are for system daemons and administrative tasks, while 100 and above are for user accounts. Since Linux administrators tend to be a fairly organized bunch, this script skips all accounts that have a uid of less than 100.
The -xdev argument to the find command ➊ ensures that find doesn’t go through all filesystems. In other words, this argument prevents the command from slogging through system areas, read-only source directories, removable devices, the /proc directory of running processes (on Linux), and similar areas. This is why we specify directories like /usr, /var, and /home explicitly. These directories are commonly on their own filesystems for backup and managerial purposes. Adding them when they reside on the same filesystem as the root filesystem doesn’t mean they will be searched twice.
It may seem at first glance that this script outputs an exceeds disk quota message for each and every account, but the awk statement after the loop ➋ only allows this message to be reported for accounts with usage greater than the predefined MAXDISKUSAGE.
This script has no arguments and should be run as root to ensure it has access to all directories and filesystems. The smart way to do this is by using the helpful sudo command (run the command man sudo in your terminal for more details). Why is sudo helpful? Because it allows you to execute one command as root, after which you will go back to being a regular user. Each time you want to run an administrative command, you have to consciously use sudo to do so. Using su - root, by contrast, makes you root for all subsequent commands until you exit the subshell, and when you get distracted, it’s all too easy to forget you are root and type in something that can lead to disaster.
NOTE
You will have to modify the directories listed in the find command ➊ to match the corresponding directories in your own disk topography.
Because this script searches across filesystems, it should be no surprise that it takes a while to run. On a large system, it could easily take somewhere between a cup of tea and a lunch with your significant other. Listing 5-2 details the results.
$ sudo fquota
User taylor exceeds disk quota. Disk usage is: 21799.4 Mbytes
Listing 5-2: Testing the fquota script
You can see that taylor is way out of control with his disk usage! His 21GB definitely exceeds the 20GB per user quota.
A complete script of this nature should have some sort of automated email capability to warn the scofflaws that they’re hogging disk space. This enhancement is demonstrated in the very next script.
Most system administrators seek the easiest way to solve a problem, and the easiest way to manage disk quotas is to extend fquota (Script #35 on page 119) to issue email warnings directly to users who are consuming too much space, as shown in Listing 5-3.
#!/bin/bash # diskhogs--Disk quota analysis tool for Unix; assumes all user # accounts are >= UID 100. Emails a message to each violating user # and reports a summary to the screen. MAXDISKUSAGE=500 ➊ violators="/tmp/diskhogs0.$$" ➋ trap "$(which rm) -f $violators" 0 ➌ for name in $(cut -d: -f1,3 /etc/passwd | awk -F: '$2 > 99 { print $1 }') do ➍ /bin/echo -n "$name " # You might need to modify the following list of directories to match the # layout of your disk. The most likely change is from /Users to /home. find / /usr /var /Users -xdev -user $name -type f -ls | \ awk '{ sum += $7 } END { print sum / (1024*1024) }' done | awk "\$2 > $MAXDISKUSAGE { print \$0 }" > $violators ➎ if [ ! -s $violators ] ; then echo "No users exceed the disk quota of ${MAXDISKUSAGE}MB" cat $violators exit 0 fi while read account usage ; do ➏ cat << EOF | fmt | mail -s "Warning: $account Exceeds Quota" $account Your disk usage is ${usage}MB, but you have been allocated only ${MAXDISKUSAGE}MB. This means that you need to delete some of your files, compress your files (see 'gzip' or 'bzip2' for powerful and easy-to-use compression programs), or talk with us about increasing your disk allocation. Thanks for your cooperation in this matter. Your afriendly neighborhood sysadmin EOF echo "Account $account has $usage MB of disk space. User notified." done < $violators exit 0
Listing 5-3: The diskhogs script
This script uses Script #35 as a base, with changes marked at ➊, ➋, ➍, ➎, and ➏. Note the addition of the fmt command in the email pipeline at ➏.
This handy trick improves the appearance of an automatically generated email when fields of unknown length, like $account, are embedded in the text. The logic of the for loop ➌ in this script is slightly different from the logic of the for loop in Script #35: because the output of the loop in this script is intended purely for the second part of the script, during each cycle, the script simply reports the account name and disk usage rather than a disk quota exceeded error message.
This script has no starting arguments and should be run as root for accurate results. This can most safely be accomplished by using the sudo command, as shown in Listing 5-4.
$ sudo diskhogs
Account ashley has 539.7MB of disk space. User notified.
Account taylor has 91799.4MB of disk space. User notified.
Listing 5-4: Testing the diskhogs script
If we now peek into the ashley account mailbox, we’ll see that a message from the script has been delivered, shown in Listing 5-5.
Subject: Warning: ashley Exceeds Quota Your disk usage is 539.7MB, but you have been allocated only 500MB. This means that you need to delete some of your files, compress your files (see 'gzip' or 'bzip2' for powerful and easy-to-use compression programs), or talk with us about increasing your disk allocation. Thanks for your cooperation in this matter. Your friendly neighborhood sysadmin
Listing 5-5: The email sent to the ashley user for being a disk hog
A useful refinement to this script would be to allow certain users to have larger quotas than others. This could easily be accomplished by creating a separate file that defines the disk quota for each user and setting a default quota in the script for users not appearing in the file. A file with account name and quota pairs could be scanned with grep and the second field extracted with a call to cut -f2.
The df utility output can be cryptic, but we can improve its readability. The script in Listing 5-6 converts the byte counts reported by df into more human-friendly units.
#!/bin/bash # newdf--A friendlier version of df awkscript="/tmp/newdf.$$" trap "rm -f $awkscript" EXIT cat << 'EOF' > $awkscript function showunit(size) ➊ { mb = size / 1024; prettymb=(int(mb * 100)) / 100; ➋ gb = mb / 1024; prettygb=(int(gb * 100)) / 100; if ( substr(size,1,1) !~ "[0-9]" || substr(size,2,1) !~ "[0-9]" ) { return size } else if ( mb < 1) { return size "K" } else if ( gb < 1) { return prettymb "M" } else { return prettygb "G" } } BEGIN { printf "%-37s %10s %7s %7s %8s %-s\n", "Filesystem", "Size", "Used", "Avail", "Capacity", "Mounted" } !/Filesystem/ { size=showunit($2); used=showunit($3); avail=showunit($4); printf "%-37s %10s %7s %7s %8s %-s\n", $1, size, used, avail, $5, $6 } EOF ➌ df -k | awk -f $awkscript exit 0
Listing 5-6: The newdf script, wrapping df so it is easier to use
Much of the work in this script takes place within an awk script, and it wouldn’t take too big of a step to write the entire script in awk rather than in the shell, using the system() function to call df directly. (Actually, this script would be an ideal candidate to rewrite in Perl, but that’s outside the scope of this book.)
There’s also an old-school trick in this script at ➊ and ➋ that comes from programming in BASIC, of all things.
When working with arbitrary-precision numeric values, a quick way to limit the number of digits after the decimal is to multiply the value by a power of 10, convert it to an integer (dropping the fractional portion), and then divide it by the same power of 10: prettymb=(int(mb * 100)) / 100;. With this code, a value like 7.085344324 becomes a much more attractive 7.08.
NOTE
Some versions of df have an -h flag that offers an output format similar to this script’s output format. However, as with many of the scripts in this book, this one will let you achieve friendly and more meaningful output on every Unix or Linux system, regardless of what version of df is present.
This script has no arguments and can be run by anyone, root or otherwise. To avoid reporting disk use on devices that you aren’t interested in, use grep -v after the call to df.
Regular df reports are difficult to understand, as shown in Listing 5-7.
$ df
Filesystem 512-blocks Used Available Capacity Mounted on
/dev/disk0s2 935761728 628835600 306414128 68% /
devfs 375 375 0 100% /dev
map -hosts 0 0 0 100% /net
map auto_home 0 0 0 100% /home
localhost:/mNhtYYw9t5GR1SlUmkgN1E 935761728 935761728 0 100% /Volumes/MobileBackups
Listing 5-7: The default output of df is convoluted and confusing.
The new script exploits awk to improve readability and knows how to convert 512-byte blocks into a more readable gigabyte format, as you can see in Listing 5-8.
$ newdf
Filesystem Size Used Avail Capacity Mounted
/dev/disk0s2 446.2G 299.86G 146.09G 68% /
devfs 187K 187K 0 100% /dev
map -hosts 0 0 0 100%
map auto_home 0 0 0 100%
localhost:/mNhtYYw9t5GR1SlUmkgN1E 446.2G 446.2G 0 100% /Volumes/MobileBackups
Listing 5-8: The easier to read and understand output of newdf
There are a number of gotchas in this script, not the least of which is that a lot of versions of df now include inode usage, and many also include processor internal information even though it’s really completely uninteresting (for example, the two map entries in the example above). In fact, this script would be far more useful if we screened those things out, so the first change you could make would be to use the -P flag in the call to df near the end of the script ➌ to remove the inode usage information. (You could also add it as a new column, but then the output would get even wider and harder to format.) In terms of removing things like the map data, that’s an easy grep, right? Simply add |grep -v "^map" at the end of ➊ and you’ll mask ’em forevermore.
While Script #37 simplified the df output to be easier to read and understand, the more basic question of how much disk space is available on the system can be addressed in a shell script. The df command reports disk usage on a per-disk basis, but the output can be a bit baffling:
$ df
Filesystem 1K-blocks Used Available Use% Mounted on
/dev/hdb2 25695892 1871048 22519564 8% /
/dev/hdb1 101089 6218 89652 7% /boot
none 127744 0 127744 0% /dev/shm
A more useful version of df would sum the “available capacity” values in column 4 and present the sum in a human-readable format. It’s a task easily accomplished with a script using the awk command, as shown in Listing 5-9.
#!/bin/bash
# diskspace--Summarizes available disk space and presents it in a logical
# and readable fashion
tempfile="/tmp/available.$$"
trap "rm -f $tempfile" EXIT
cat << 'EOF' > $tempfile
{ sum += $4 }
END { mb = sum / 1024
gb = mb / 1024
printf "%.0f MB (%.2fGB) of available disk space\n", mb, gb
}
EOF
➊ df -k | awk -f $tempfile
exit 0
Listing 5-9: The diskspace script, a handy wrapper with friendlier output to df
The diskspace shell script relies mainly on a temporary awk script that is written to the /tmp directory. This awk script calculates the total amount of disk space left using data fed to it and then prints the result in a user-friendly format. The output of df is then piped through awk ➊, which performs the actions in the awk script. When execution of the script is finished, the temporary awk script is removed from the /tmp directory because of the trap command run at the beginning of the script.
This script, which can be run as any user, produces a succinct one-line summary of available disk space.
For the same system that generated the earlier df output, this script reports output similar to that shown in Listing 5-10.
$ diskspace
96199 MB (93.94GB) of available disk space
Listing 5-10: Testing the diskspace script
If your system has lots of disk space across many multiterabyte drives, you might expand this script to automatically return values in terabytes as needed. If you’re just out of space, it’ll doubtlessly be discouraging to see 0.03GB of available disk space—but that’s a good incentive to use Script #36 on page 121 and clean things up, right?
Another issue to consider is whether it’s more useful to know about the available disk space on all devices, including those partitions that cannot grow, like /boot, or whether it’s enough to report on just user volumes. If the latter, you can improve this script by making a call to grep immediately after the df call ➊. Use grep with the desired device names to include only particular devices, or use grep -v followed by the unwanted device names to screen out devices you don’t want included.
The locate script, Script #19 on page 68, is useful but has a security problem: if the build process is run as root, it builds a list of all files and directories on the entire system, regardless of owner, allowing users to see directories and filenames that they wouldn’t otherwise have permission to access. The build process can be run as a generic user (as OS X does, running mklocatedb as user nobody), but that’s not right either, because you want to be able to locate file matches anywhere in your directory tree, regardless of whether user nobody has access to those particular files and directories.
One way to solve this dilemma is to increase the data saved in the locate database so that each entry has an owner, group, and permissions string attached. But then the mklocatedb database itself remains insecure, unless the locate script is run as either a setuid or setgid script, and that’s something to be avoided at all costs in the interest of system security.
A compromise is to have a separate .locatedb file for each user. This isn’t too bad of an option, because a personal database is needed only for users who actually use the locate command. Once invoked, the system creates a .locatedb file in the user’s home directory, and a cron job can update existing .locatedb files nightly to keep them in sync. The very first time someone runs the secure slocate script, it outputs a message warning them that they may see only matches for files that are publicly accessible. Starting the very next day (depending on the cron schedule), the users get their personalized results.
Two scripts are necessary for a secure locate: the database builder, mkslocatedb (shown in Listing 5-11) and the actual search utility, slocate (shown in Listing 5-12).
#!/bin/bash
# mkslocatedb--Builds the central, public locate database as user nobody
# and simultaneously steps through each user's home directory to find
# those that contain a .slocatedb file. If found, an additional, private
# version of the locate database will be created for that user.
locatedb="/var/locate.db"
slocatedb=".slocatedb"
if [ "$(id -nu)" != "root" ] ; then
echo "$0: Error: You must be root to run this command." >&2
exit 1
fi
if [ "$(grep '^nobody:' /etc/passwd)" = "" ] ; then
echo "$0: Error: you must have an account for user 'nobody'" >&2
echo "to create the default slocate database." >&2
exit 1
fi
cd / # Sidestep post-su pwd permission problems.
# First create or update the public database.
➊ su -fm nobody -c "find / -print" > $locatedb 2>/dev/null
echo "building default slocate database (user = nobody)"
echo ... result is $(wc -l < $locatedb) lines long.
# Now step through the user accounts on the system to see who has
# a .slocatedb file in their home directory.
for account in $(cut -d: -f1 /etc/passwd)
do
homedir="$(grep "^${account}:" /etc/passwd | cut -d: -f6)"
if [ "$homedir" = "/" ] ; then
continue # Refuse to build one for root dir.
elif [ -e $homedir/$slocatedb ] ; then
echo "building slocate database for user $account"
su -m $account -c "find / -print" > $homedir/$slocatedb \
2>/dev/null
chmod 600 $homedir/$slocatedb
chown $account $homedir/$slocatedb
echo ... result is $(wc -l < $homedir/$slocatedb) lines long.
fi
done
exit 0
Listing 5-11: The mkslocatedb script
The slocate script itself (shown in Listing 5-12) is the user interface to the slocate database.
#!/bin/bash # slocate--Tries to search the user's own secure locatedb database for the # specified pattern. If the pattern doesn't match, it means no database # exists, so it outputs a warning and creates one. If personal .slocatedb # is empty, it uses system database instead. locatedb="/var/locate.db" slocatedb="$HOME/.slocatedb" if [ ! -e $slocatedb -o "$1" = "--explain" ] ; then cat << "EOF" >&2 Warning: Secure locate keeps a private database for each user, and your database hasn't yet been created. Until it is (probably late tonight), I'll just use the public locate database, which will show you all publicly accessible matches rather than those explicitly available to account ${USER:-$LOGNAME}. EOF if [ "$1" = "--explain" ] ; then exit 0 fi # Before we go, create a .slocatedb file so that cron will fill it # the next time the mkslocatedb script is run. touch $slocatedb # mkslocatedb will build it next time through. chmod 600 $slocatedb # Start on the right foot with permissions. elif [ -s $slocatedb ] ; then locatedb=$slocatedb else echo "Warning: using public database. Use \"$0 --explain\" for details." >&2 fi if [ -z "$1" ] ; then echo "Usage: $0 pattern" >&2 exit 1 fi exec grep -i "$1" $locatedb
Listing 5-12: The slocate script, the companion script to mkslocatedb
The mkslocatedb script revolves around the idea that a process running as root can temporarily become owned by a different user ID by using su -fm user ➊. It can then run find on the filesystem of each user as that user in order to create a user-specific database of filenames. Working with the su command proves tricky within this script, though, because by default, su not only wants to change the effective user ID but also wants to import the environment of the specified account. The end result is odd and confusing error messages on just about any Unix unless the -m flag is specified, which prevents the user environment from being imported. The -f flag is extra insurance, bypassing the .cshrc file for any csh or tcsh users.
The other unusual notation at ➊ is 2>/dev/null, which routes all error messages directly to the proverbial bit bucket: anything redirected to /dev/null vanishes without a trace. This is an easy way to skip the inevitable flood of permission denied error messages for each find function invoked.
The mkslocatedb script is unusual in that not only must it be run as root, but using sudo won’t cut it. You need to either log in as root or use the more powerful su command to become root before running the script. This is because su will actually switch you to the root user in order to run the script, in contrast to sudo, which simply grants the current user root privileges. sudo can result in different permissions being set on files than su does. The slocate script, of course, has no such requirements.
Building the slocate database for both nobody (the public database) and user taylor on a Linux box produces the output shown in Listing 5-13.
# mkslocatedb
building default slocate database (user = nobody)
... result is 99809 lines long.
building slocate database for user taylor
... result is 99808 lines long.
Listing 5-13: Running the mkslocatedb script as root
To search for a particular file or set of files that match a given pattern, let’s first try it as user tintin (who doesn’t have a .slocatedb file):
tintin $ slocate Taylor-Self-Assess.doc
Warning: using public database. Use "slocate --explain" for details.
$
Now we’ll enter the same command, but as user taylor, who owns the file being sought:
taylor $ slocate Taylor-Self-Assess.doc
/Users/taylor/Documents/Merrick/Taylor-Self-Assess.doc
If you have a very large filesystem, it’s possible that this approach will consume a nontrivial amount of space. One way to address this issue is to make sure that the individual .slocatedb database files don’t contain entries that also appear in the central database. This requires a bit more processing up front (sort both and then use diff, or simply skip /usr and /bin when searching for individual user files), but it could pay off in terms of saved space.
Another technique for saving space is to build the individual .slocatedb files with references only to files that have been accessed since the last update. This works better if the mkslocatedb script is run weekly rather than daily; otherwise, each Monday all users would be back to ground zero because they’d be unlikely to have run the slocate command over the weekend.
Finally, another easy way to save space would be to keep the .slocatedb files compressed and uncompress them on the fly when they are searched with slocate. See the zgrep command in Script #33 on page 109 for inspiration regarding how to do this.
If you’re responsible for managing a network of Unix or Linux systems, you’ve already experienced the frustration caused by subtle incompatibilities among the different operating systems in your dominion. Some of the most basic administration tasks prove to be the most incompatible across different flavors of Unix, and chief among these tasks is user account management. Rather than have a single command line interface that is 100 percent consistent across all Linux flavors, each vendor has developed its own graphical interface for working with the peculiarities of its own system.
The Simple Network Management Protocol (SNMP) was ostensibly supposed to help normalize this sort of thing, but managing user accounts is just as difficult now as it was a decade ago, particularly in a heterogeneous computing environment. As a result, a very helpful set of scripts for a system administrator includes a version of adduser, suspenduser, and deleteuser that can be customized for your specific needs and then easily ported to all of your Unix systems. We’ll show you adduser here and cover suspenduser and deleteuser in the next two scripts.
NOTE
OS X is an exception to this rule, with its reliance on a separate user account database. To retain your sanity, just use the Mac versions of these commands and don’t try to figure out the byzantine command line access that they sort of grant administrative users.
On a Linux system, an account is created by adding a unique entry to the /etc/passwd file, consisting of a one- to eight-character account name, a unique user ID, a group ID, a home directory, and a login shell for that user. Modern systems store the encrypted password value in /etc/shadow, so a new user entry must be added to that file, too. Finally, the account needs to be listed in the /etc/group file, with the user either as their own group (the strategy implemented in this script) or as part of an existing group. Listing 5-14 shows how we can accomplish all of these steps.
#!/bin/bash
# adduser--Adds a new user to the system, including building their
# home directory, copying in default config data, etc.
# For a standard Unix/Linux system, not OS X.
pwfile="/etc/passwd"
shadowfile="/etc/shadow"
gfile="/etc/group"
hdir="/home"
if [ "$(id -un)" != "root" ] ; then
echo "Error: You must be root to run this command." >&2
exit 1
fi
echo "Add new user account to $(hostname)"
/bin/echo -n "login: " ; read login
# The next line sets the highest possible user ID value at 5000,
# but you should adjust this number to match the top end
# of your user ID range.
➊ uid="$(awk -F: '{ if (big < $3 && $3 < 5000) big=$3 } END { print big + 1 }'\
$pwfile)"
homedir=$hdir/$login
# We are giving each user their own group.
gid=$uid
/bin/echo -n "full name: " ; read fullname
/bin/echo -n "shell: " ; read shell
echo "Setting up account $login for $fullname..."
echo ${login}:x:${uid}:${gid}:${fullname}:${homedir}:$shell >> $pwfile
echo ${login}:*:11647:0:99999:7::: >> $shadowfile
echo "${login}:x:${gid}:$login" >> $gfile
mkdir $homedir
cp -R /etc/skel/.[a-zA-Z]* $homedir
chmod 755 $homedir
chown -R ${login}:${login} $homedir
# Setting an initial password
aexec passwd $login
Listing 5-14: The adduser script
The coolest single line in this script is at ➊. This scans through the /etc/passwd file to figure out the largest user ID currently in use that’s less than the highest allowable user account value (this script uses 5000, but you should adjust this for your own configuration) and then adds 1 to it for the new account user ID. This saves the admin from having to remember what the next available ID is, and it also offers a high degree of consistency in account information as the user community evolves and changes.
The script creates an account with this user ID. Then it creates the account’s home directory and copies into it the contents of the /etc/skel directory. By convention, the /etc/skel directory is where a master .cshrc, .login, .bashrc, and .profile are kept, and on sites where there’s a web server offering ~account service, a directory like /etc/skel/public_html would also be copied across to the new home directory. This is super useful if your organization provisions Linux workstations or accounts with special bash configurations for engineers or developers.
This script must be run by root and has no starting arguments.
Our system already has an account named tintin, so we’ll ensure that snowy1 has his own account too (shown in Listing 5-15).
$ sudo adduser Add new user account to aurora login: snowy full name: Snowy the Dog shell: /bin/bash Setting up account snowy for Snowy the Dog... Changing password for user snowy. New password: Retype new password: passwd: all authentication tokens updated successfully.
Listing 5-15: Testing the adduser script
One significant advantage of using your own adduser script is that you can add code and change the logic of certain operations without worrying about an OS upgrade stepping on the modifications. Possible modifications include automatically sending a welcome email that outlines usage guidelines and online help options, automatically printing out an account information sheet that can be routed to the user, adding a firstname_lastname or firstname.lastname alias to the mail aliases file, or even copying a set of files into the account so that the owner can immediately begin to work on a team project.
Whether a user is being escorted off the premises for industrial espionage, a student is taking the summer off, or a contractor is going on hiatus, there are many times when it’s useful to disable an account without actually deleting it from the system.
This can be done simply by changing the user’s password to a new value that they aren’t told, but if the user is logged in at the time, it’s also important to log them out and shut off access to that home directory from other accounts on the system. When an account is suspended, odds are very good that the user needs to be off the system now—not when they feel like it.
Much of the script in Listing 5-16 revolves around ascertaining whether the user is logged in, notifying the user that they are being logged off, and kicking the user off the system.
#!/bin/bash # suspenduser--Suspends a user account for the indefinite future homedir="/home" # Home directory for users secs=10 # Seconds before user is logged out if [ -z $1 ] ; then echo "Usage: $0 account" >&2 exit 1 elif [ "$(id -un)" != "root" ] ; then echo "Error. You must be 'root' to run this command." >&2 exit 1 fi echo "Please change the password for account $1 to something new." passwd $1 # Now let's see if they're logged in and, if so, boot 'em. if who|grep "$1" > /dev/null ; then for tty in $(who | grep $1 | awk '{print $2}'); do cat << "EOF" > /dev/$tty ****************************************************************************** URGENT NOTICE FROM THE ADMINISTRATOR: This account is being suspended, and you are going to be logged out in $secs seconds. Please immediately shut down any processes you have running and log out. If you have any questions, please contact your supervisor or John Doe, Director of Information Technology. ****************************************************************************** EOF done echo "(Warned $1, now sleeping $secs seconds)" sleep $secs jobs=$(ps -u $1 | cut -d\ -f1) ➊ kill -s HUP $jobs # Send hangup sig to their processes. sleep 1 # Give it a second... ➋ kill -s KILL $jobs > /dev/null 2>1 # and kill anything left. echo "$1 was logged in. Just logged them out." fi # Finally, let's close off their home directory from prying eyes. chmod 000 $homedir/$1 echo "Account $1 has been suspended." exit 0
Listing 5-16: The suspenduser script
This script changes the user’s password to a value unknown to the user and then shuts off the user’s home directory. If they are logged in, we give a few seconds’ warning and then log the user out by killing all of their running processes.
Notice how the script sends the SIGHUP (HUP) hang-up signal to each running process ➊ and then waits a second before sending the more aggressive SIGKILL (KILL) signal ➋. The SIGHUP signal quits running applications—except not always, and it won’t kill a login shell. The SIGKILL signal, however, can’t be ignored or blocked, so it’s guaranteed to be 100 percent effective. It’s not preferred, though, because it doesn’t give the application any time to clean up temporary files, flush file buffers to ensure that changes are written to disk, and so forth.
Unsuspending a user is a simple two-step process of opening their home directory back up (with chmod 700) and resetting the password to a known value (with passwd).
This script must be run as root, and it has one argument: the name of the account to suspend.
It turns out that snowy has already been abusing his account. Let’s suspend him, as shown in Listing 5-17.
$ sudo suspenduser snowy
Please change the password for account snowy to something new.
Changing password for user snowy.
New password:
Retype new password:
passwd: all authentication tokens updated successfully.
(Warned snowy, now sleeping 10 seconds)
snowy was logged in. Just logged them out.
Account snowy has been suspended.
Listing 5-17: Testing the suspenduser script on the user snowy
Since snowy was logged in at the time, Listing 5-18 shows what he saw on his screen just seconds before he was kicked off the system.
****************************************************************************** URGENT NOTICE FROM THE ADMINISTRATOR: This account is being suspended, and you are going to be logged out in 10 seconds. Please immediately shut down any processes you have running and log out. If you have any questions, please contact your supervisor or John Doe, Director of Information Technology. ******************************************************************************
Listing 5-18: The warning printed to a user’s terminals before they are suspended
Deleting an account is a bit more tricky than suspending it, because the script needs to check the entire filesystem for files owned by the user before the account information is removed from /etc/passwd and /etc/shadow. Listing 5-19 ensures a user and their data are fully deleted from the system. It expects the previous suspenduser script is in the current PATH.
#!/bin/bash # deleteuser--Deletes a user account without a trace. # Not for use with OS X. homedir="/home" pwfile="/etc/passwd" shadow="/etc/shadow" newpwfile="/etc/passwd.new" newshadow="/etc/shadow.new" suspend="$(which suspenduser)" locker="/etc/passwd.lock" if [ -z $1 ] ; then echo "Usage: $0 account" >&2 exit 1 elif [ "$(whoami)" != "root" ] ; then echo "Error: you must be 'root' to run this command.">&2 exit 1 fi $suspend $1 # Suspend their account while we do the dirty work. uid="$(grep -E "^${1}:" $pwfile | cut -d: -f3)" if [ -z $uid ] ; then echo "Error: no account $1 found in $pwfile" >&2 exit 1 fi # Remove the user from the password and shadow files. grep -vE "^${1}:" $pwfile > $newpwfile grep -vE "^${1}:" $shadow > $newshadow lockcmd="$(which lockfile)" # Find lockfile app in the path. ➊ if [ ! -z $lockcmd ] ; then # Let's use the system lockfile. eval $lockcmd -r 15 $locker else # Ulp, let's do it ourselves. ➋ while [ -e $locker ] ; do echo "waiting for the password file" ; sleep 1 done ➌ touch $locker # Create a file-based lock. fi mv $newpwfile $pwfile mv $newshadow $shadow ➍ rm -f $locker # Click! Unlocked again. chmod 644 $pwfile chmod 400 $shadow # Now remove home directory and list anything left. rm -rf $homedir/$1 echo "Files still left to remove (if any):" find / -uid $uid -print 2>/dev/null | sed 's/^/ /' echo "" echo "Account $1 (uid $uid) has been deleted, and their home directory " echo "($homedir/$1) has been removed." exit 0
Listing 5-19: The deleteuser script
To avoid anything changing in the to-be-suspended user’s account while the script is working, the very first task that deleteuser performs is to suspend the user account by calling suspenduser.
Before modifying the password file, this script locks it using the lockfile program if it’s available ➊. Alternatively, on Linux you could also look into using the flock utility for creating a file lock. If not, the script drops back to a relatively primitive semaphore locking mechanism through the creation of the file /etc/passwd.lock. If the lock file already exists ➋, this script will wait for it to be deleted by another program; once it’s gone, deleteuser immediately creates it and proceeds ➌, deleting it when done ➍.
This script must be run as root (use sudo) and needs the name of the account to delete as a command argument. Listing 5-20 shows the script being run on the user snowy.
WARNING
This script is irreversible and causes lots of files to vanish, so be careful if you want to experiment with it!
$ sudo deleteuser snowy
Please change the password for account snowy to something new.
Changing password for user snowy.
New password:
Retype new password:
passwd: all authentication tokens updated successfully.
Account snowy has been suspended.
Files still left to remove (if any):
/var/log/dogbone.avi
Account snowy (uid 502) has been deleted, and their home directory
(/home/snowy) has been removed.
Listing 5-20: Testing the deleteuser script on the user snowy
That sneaky snowy had hidden an AVI file (dogbone.avi) in /var/log. Luckily we noticed that—who knows what it could be?
This deleteuser script is deliberately incomplete. You should decide what additional steps to take—whether to compress and archive a final copy of the account files, write them to tape, back them up on a cloud service, burn them to a DVD-ROM, or even mail them directly to the FBI (hopefully we’re just kidding on that last one). In addition, the account needs to be removed from the /etc/group files. If there are stray files outside of the user’s home directory, the find command identifies them, but it’s still up to the sysadmin to examine and delete each one as appropriate.
Another useful addition to this script would be a dry-run mode, allowing you to see what the script would remove from the system before actually performing the user deletion.
Because people migrate their login, profile, and other shell environment customizations from one system to another, it’s not uncommon for these settings to progressively decay; eventually, the PATH can include directories that aren’t on the system, the PAGER can point to a nonexistent binary, and worse.
A sophisticated solution to this problem is to first check the PATH to ensure that it includes only valid directories on the system, and then to check each of the key helper application settings to ensure that they’re either indicating a fully qualified file that exists or are specifying a binary that’s in the PATH. This is detailed in Listing 5-21.
#!/bin/bash # validator--Ensures that the PATH contains only valid directories # and then checks that all environment variables are valid. # Looks at SHELL, HOME, PATH, EDITOR, MAIL, and PAGER. errors=0 ➊ source library.sh # This contains Script #1, the in_path() function. ➋ validate() { varname=$1 varvalue=$2 if [ ! -z $varvalue ] ; then ➌ if [ "${varvalue%${varvalue#?}}" = "/" ] ; then if [ ! -x $varvalue ] ; then echo "** $varname set to $varvalue, but I cannot find executable." (( errors++ )) fi else if in_path $varvalue $PATH ; then echo "** $varname set to $varvalue, but I cannot find it in PATH." errors=$(( $errors + 1 )) fi fi fi } # BEGIN MAIN SCRIPT # ================= ➍ if [ ! -x ${SHELL:?"Cannot proceed without SHELL being defined."} ] ; then echo "** SHELL set to $SHELL, but I cannot find that executable." errors=$(( $errors + 1 )) fi if [ ! -d ${HOME:?"You need to have your HOME set to your home directory"} ] then echo "** HOME set to $HOME, but it's not a directory." errors=$(( $errors + 1 )) fi # Our first interesting test: Are all the paths in PATH valid? ➎ oldIFS=$IFS; IFS=":" # IFS is the field separator. We'll change to ':'. ➏ for directory in $PATH do if [ ! -d $directory ] ; then echo "** PATH contains invalid directory $directory." errors=$(( $errors + 1 )) fi done IFS=$oldIFS # Restore value for rest of script. # The following variables should each be a fully qualified path, # but they may be either undefined or a progname. Add additional # variables as necessary for your site and user community. validate "EDITOR" $EDITOR validate "MAILER" $MAILER validate "PAGER" $PAGER # And, finally, a different ending depending on whether errors > 0 if [ $errors -gt 0 ] ; then echo "Errors encountered. Please notify sysadmin for help." else echo "Your environment checks out fine." fi exit 0
Listing 5-21: The validator script
The tests performed by this script aren’t overly complex. To check that all the directories in PATH are valid, the code steps through each directory to ensure that it exists ➏. Notice that the internal field separator (IFS) had to be changed to a colon at ➎ so that the script would properly step through all of the PATH directories. By convention, the PATH variable uses a colon to separate each of its directories:
$ echo $PATH
/bin/:/sbin:/usr/bin:/sw/bin:/usr/X11R6/bin:/usr/local/mybin
To validate that the environment variable values are valid, the validate() function ➋ first checks whether each value begins with a /. If it does, the function checks whether the variable is an executable. If it doesn’t begin with a /, the script calls the in_path() function imported from the library we started with Script #1 on page 11 ➊ to see whether the program is found in one of the directories in the current PATH.
The most unusual aspects of this script are its use of default values within some of the conditionals and its use of variable slicing. Its use of default values in the conditionals is exemplified by the line at ➍. The notation ${varname:?"errorMessage"} can be read as “If varname exists, substitute its value; otherwise, fail with the error errorMessage.”
The variable-slicing notation ${varvalue%${varvalue#?}} used at ➌ is the POSIX substring function, and it produces only the first character of the variable varvalue. In this script, it’s used to tell whether an environment variable has a fully qualified filename (one starting with / and specifying the path to the binary).
If your version of Unix/Linux doesn’t support either of these notations, they can be replaced in a straightforward fashion. For example, instead of ${SHELL:?No Shell}, you could substitute the following:
if [ -z "$SHELL" ] ; then echo "No Shell" >&2; exit 1 fi
And instead of {varvalue%${varvalue#?}}, you could use this code to accomplish the same result:
$(echo $varvalue | cut -c1)
This is code that users can run to check their own environment. There are no starting arguments, as Listing 5-22 shows.
$ validator
** PATH contains invalid directory /usr/local/mybin.
** MAILER set to /usr/local/bin/elm, but I cannot find executable.
Errors encountered. Please notify sysadmin for help.
Listing 5-22: Testing the validator script
Although many sites disable the guest user for security reasons, others do have a guest account (often with a trivially guessable password) to allow clients or people from other departments to access the network. It’s a useful account, but there’s one big problem: with multiple people sharing the same account, it’s easy for someone to leave things messed up for the next user—maybe they were experimenting with commands, editing .rc files, adding subdirectories, or so forth.
This script in Listing 5-23 addresses the problem by cleaning up the account space each time a user logs out of the guest account. It deletes any newly created files or subdirectories, removes all dotfiles, and rebuilds the official account files, copies of which are stored in a read-only archive tucked away in the guest account’s .template directory.
#!/bin/bash # fixguest--Cleans up the guest account during the logout process # Don't trust environment variables: reference read-only sources. iam=$(id -un) myhome="$(grep "^${iam}:" /etc/passwd | cut -d: -f6)" # *** Do NOT run this script on a regular user account! if [ "$iam" != "guest" ] ; then echo "Error: you really don't want to run fixguest on this account." >&2 exit 1 fi if [ ! -d $myhome/..template ] ; then echo "$0: no template directory found for rebuilding." >&2 exit 1 fi # Remove all files and directories in the home account. cd $myhome rm -rf * $(find . -name ".[a-zA-Z0-9]*" -print) # Now the only thing present should be the ..template directory. cp -Rp ..template/* . exit 0
Listing 5-23: The fixguest script
For this script to work correctly, you’ll want to create a master set of template files and directories within the guest home directory, tucked into a new directory called ..template. Change the permissions of the ..template directory to be read-only and then ensure that all the files and directories within ..template have the proper ownership and permissions for user guest.
A logical time to run the fixguest script is at logout, by invoking it in the .logout file (which works with most shells, though not all). Also, it’ll doubtless save you lots of complaints from users if the login script outputs a message like this one:
Notice: All files are purged from the guest account immediately upon logout, so please don't save anything here you need. If you want to save something, email it to your main account instead. You've been warned!
However, because some guest users might be savvy enough to tinker with the .logout file, it would be worthwhile to invoke the fixguest script from cron too. Just make sure no one is logged into the account when it runs!
There are no visible results from running this program, except that the guest home directory is restored to mirror the layout and files in the ..template directory.