4

System Maintenance

The last two chapters covered in considerable detail various approaches to installing, running, and maintaining Perl scripts on NT workstations. Thus far, however, these scripts haven't actually done anything useful; in fact, they haven't done anything at all! In this chapter, we begin to suggest some of the functions that these scripts might perform in a real-world environment.

In one sense, the answer to the question “What tasks can be scripted with Perl?” is “Anything.” Any task involving the use of command-line utilities or registry tweaks can be scripted with ease. In addition, readily available are several Perl modules that provide a very usable interface to all sorts of Windows features that would normally be available only through the GUI interface or to a Windows programmer.* A more useful question to ask than what can be scripted is what can usefully be scripted with Perl. Clearly, the answer depends on the specifics of a given computing environment; no two sites are identical, and no one has identical needs. However, there are general rules of thumb to bear in mind when deciding whether to script a task:

  • Is the task error-prone? For example, does it involve changing obscure registry values? If so, would the consequences of an error be serious?
  • Is the task one that needs to be carried out regularly or on a lot of machines?
  • Can the task be accomplished without the use of GUI tools? Even if GUI tools are normally used, can the same effect be accomplished through command-line utilities, registry tweaks, or configuration file changes?

If the answer to any of these questions is yes, then it is probably worth writing a script to automate the task. Writing a script to accomplish a task invariably takes considerably longer than performing it manually; the payoff is that it needs to be written only once.

There are two major areas of workstation administration in which scripts can be totally invaluable: the first is when a set of mechanical maintenance tasks have to be carried out on a regular basis; the second is when configuration changes need to be made to large numbers of machines. In this chapter, we concentrate on the first category; a large part of the rest of the book is dedicated to the more complex but much more powerful configuration issues. Broadly, system maintenance consists of housekeeping and reporting, which are considered here.

Housekeeping

Any task that involves keeping a workstation running tidily and smoothly can generally be thought of as housekeeping. On a Windows NT workstation, this involves tasks such as ensuring that temporary directories don't get filled up, that system logs are archived, that disks are defragmented and free from virus infections, and so forth. On a workstation that is not tightly locked down, this might extend to tasks such as checking that specific directories in the file system contain what they should or that a workstation is not advertising huge numbers of unnecessary shares. Some of these tasks can be automated without scripting—for example, many virus checkers include their own scheduling utilities—but even in these cases, there is a lot to be said for managing all automated tasks from one central place, namely a single, easily maintainable script. As we have already pointed out, the exact nature of the sorts of tasks you will need to carry out will, of course, depend on your specific environment. However, two examples of scripts that have general utility follow. The first involves the use of command-line utilities; the second demonstrates the power of Windows-specific Perl modules. As ever, you are strongly encouraged to adapt this code, developing your own ideas.

Cleaning Temporary Directories

Windows likes its temporary directories, as do many Windows applications. While some of these are very good at managing space, meticulously deleting files when they are no longer required, there are plenty that are not so good. Also, even the most fastidious program will often leave temporary files lying around if the system crashes or the program is forced to quit unexpectedly. Slowly but surely, temporary directories increase in size over time, often creating a decrease in system performance and reliability. If end users are also using these directories (even inadvertently) to store Internet downloads or other detritus, the situation is exacerbated more quickly. Making sure that this does not cause problems by deleting the contents of such directories regularly is a task for which scripts are ideally suited.

Here is an extremely simple scriptlet for recursively deleting the contents of specified directories. For each directory, it ensures that the files are deletable (using NT's attrib.exe command-line utility) and then uses the command-line del to purge the contents.* To run it from the command line, simply save it in a file with a .pl extension. In common with most of the scripts in the book, however, it can more usefully be run using one of the techniques described in Chapter 2, Running a Script Without User Intervention, and Chapter 3, Remote Script Management.

@dirs =('C:\temp','C:\tmp');
foreach $dir(@dirs)
{
    ‘attrib /s $dir\\*.* -r -s -h‘;
    ‘del /f /s /q $dir\\*.*‘;
}

This scriplet is hardly revolutionary. The same effect could have been achieved easily by placing the relevant attrib and del commands into a batch file and running it, say, from a scheduler. Perl comes into its own, however, when a little more flexibility is desired. The following scriptlet, for example, does exactly the same thing as the previous one, except that it deletes the contents only of directories specified in a file called C:\admin\purgabledirs. If this file does not exist, the script does nothing.

$filename = 'C:\admin\purgabledirs';
if(-e $filename)
{
     open FILE,$filename;
     while($dir = <FILE>)
     {
        chomp($dir);
        ‘attrib /s $dir\\*.* -r -s -h‘;
        ‘del /f /s /q $dir\\*.*‘;
     }
     close FILE;
}

If we wish to delete files of a certain type, say, files with a .dat extension, the only modification we need to make to either of the preceding scripts is to change the *.* wildcard to read *.dat. Suppose, however, that we wish to purge all files except those with a .dat extension (which is not an unlikely scenario). This is a more involved problem because we have to step through each file in each directory recursively, deciding whether to delete it. As you will remember from the previous chapter, Perl has a very useful readdir() function that can be used to loop through each file; a regular expression could soon tell us if a file matched our deletion criteria.

One slight complication is how to deal with nested subdirectories. Each needs its contents examined before being deleted, which could make program flow rather tricky. An elegant solution to this problem is to write a Perl function and call it recursively. A function is simply a named block of code that can be called from anywhere in the script when required; a function can even be invoked from within itself! This is exactly what we need to recursively delete directories. We define a function called EraseContents that reads the contents of a directory, checking each file in turn, and deleting it if the deletion criteria are matched. The clever bit is that if one of the files is actually a directory, it recursively invokes the EraseContents function on that directory before attempting to remove it.

The following script demonstrates use of a recursive function call to delete all files except those with a .dat extension from a directory tree. We hope you will agree that writing and invoking a function is an extremely simple process. There are two incantations that might need explaining, however. First, the symbol @_[0] means the first parameter passed to the function. Second, the keyword my means that the variable following it is local to the function; each invocation of EraseContents contains its own copy of such variables, and they do not interfere with each other. Finally, it is worth noting that script execution actually begins at the bottom line, where we invoke the function for the first time, passing C:\temp as a parameter.

sub EraseContents
{
    opendir DIR,$_[0];
    my @files = readdir(DIR);
    closedir DIR;

The function has been declared and the directory contents read into an array. Now go through each element of the array in turn, setting $filename to be the fully qualified path of the file in question.

foreach my $file(@files)
{
    my $filename = "$_[0]\\$file";

If it's a directory, and it is not one of the special directories (. or ..), make a recursive call and then remove the directory. (If the directory is not empty, rmdir will do nothing.)

if(-d $filename)
{
unless($filename =~ /\\\.+$/)
     {
          EraseContents ($filename);
          rmdir $filename;
     }
 }

If it's a normal file, check that it doesn't match our exception criterion. If it doesn't, delete it.

else
    {
        unless($filename =~ /\.dat$/)
        {
            ‘del $filename‘;
        }
    }
  }
}

The script execution starts here:

EraseContents "C:\temp";

And that's all there is to it!* Note that before making any recursive calls, we check that we are not dealing with the . or .. directories. This is extremely important. If that line were omitted, the script would recursively call EraseContents forever or at least until it gave up, because all directories possess these two directories, so they will always be the top two entries in our directory array. However, if the script were slightly different (or even just contained an error), you might end up deleting a whole directory tree below and above your starting point—think about it, .. means “the parent directory”!

When testing and debugging scripts that can potentially cause a lot of damage, such as the preceding one, always take precautions. Either try the script on a workstation that can be trashed without a problem or at the very least use a subst command to ensure that you are dealing with a logical directory structure that you know you don't care about. There are a variety of techniques you can use to try to ensure that your scripts don't do anything too rash; for more information, see Chapter 8, Script Safety and Security.

Archiving NT Event Logs

Temporary directories are not the only things that can fill up on Windows NT; so can event logs. These logs are used by the operating system itself and by any other application that registers itself appropriately; they are normally read (either locally or remotely) using the Event Viewer program. Typically, the sorts of things stored in the event log are attempted security infringements, driver failures, and the like. (These logs are discussed in much more detail in the next chapter.) Once one of the event logs has reached its maximum size, its behavior depends upon how the workstation is configured. There are three possible settings:

  • Start to overwrite old entries as required.
  • Do not overwrite anything. Instead, fail to log new events.
  • A hybrid: overwrite entries older than a certain number of days, then stop logging new events.

Clearly none of these scenarios is particularly desirable, because as an administrator you never know when you may need to look at a log entry, even an old one. For example, if someone has breached the security on your network, workstation event logs might provide invaluable information about where and when the attack started. Therefore, making sure logs are cleared regularly and keeping archives of old ones can, in many environments, be an essential workstation maintenance task. Unfortunately, NT does not come with any command-line tools to facilitate this process, but with the help of the Win32::EventLog module (see the Appendix, Perl Module Functions), it is a task that can be readily scripted using Perl.*

Following is an example of such a script. It starts by attempting to create the backup directory specified by $backdirectory (unless this directory already exists) and uses the Win32::EventLog module to back up and clear all three event logs.

First, we import the module and set up the $backdirectory variable:

use Win32::EventLog;
$backdirectory = 'C:\eventlogbackups';

Next, we use a combination of Perl's built-in time and localtime functions to fill a list of variables with information about the current time. These values are later used as part of the archive filename. Note that the value of $month is incremented by one; this is because localtime refers to January as month 0 rather than month 1.

if (!-d $backdirectory)
{
    ‘mkdir $backdirectory‘;
}
($sec,$min,$hour,$day,$month,$year) = localtime(time);
$month++;

Now we create a hash table, where each of the keys corresponds to one of the three event logs and where the associated value is a three-letter abbreviation. Then, for each of the keys, we construct a string out of the path of the workstation's newly created directory on the server drive, the three-letter abbreviation associated with the log (this value is read from the hash table), and the current day and month. We then open the relevant log and invoke the Clear method, passing our newly constructed string as a filename.*

%logtypes = ("system","sys","security","sec","applications","app");
     foreach $key (keys %logtypes)
     {
         $logname = "$backdirectory\\$logtypes{$key}_$day$month";
         $evtlog = Win32::EventLog->new($key,$ENV{COMPUTERNAME});
         $evtlog->Clear($logname);
     }

All three event logs are now archived and empty.

Reporting

Scripts such as the ones described previously can be valuable tools in any workstation maintenance regime, because they can deal with many common workstation problems before they become serious. Inevitably, however, there are going to be circumstances in which mechanized “housekeeping” solutions will fail and an administrator will have to intervene manually. For example, a hard disk may become dangerously full due to a buildup of files in a location not covered by an automatic-deletion script. Alternatively, a workstation can suffer from a hardware failure or software conflict that no amount of automated housekeeping could predict. Thankfully, that something is unpredictable does not necessarily make it undetectable. Even if a script cannot automatically fix a problem, it may be able to detect it and warn an administrator before any serious consequences occur. Reporting, then, consists of carrying out various checks on a workstation and informing the system administrator when a problem arises.

In an ideal world, a single script would be able to detect and report absolutely any problem. Unfortunately, however, this is totally impossible: the best you can do is write a script that detects certain sorts of problems that you think might occur. Just as with housekeeping, the specific problems that would benefit from automatic detection vary from environment to environment. We therefore give two examples that could be used in most environments and that are representative of the sorts of techniques you might employ when writing your own versions. The two tasks we've chosen are analogous to those shown in the preceding housekeeping section: the first reports when disks are becoming dangerously full; the second reports any warning or error that appears in the workstation event logs.

Reporting Disk Overload

As we have already mentioned, a common cause of performance problems and instability in Windows NT is a full hard disk, particularly if the disk in question contains the system partition. This is an easy problem to detect in an automated fashion and therefore an ideal candidate for scripting. Thanks to the Perl Net::SMTP module,* getting a script to send an email is simple. Information about the amount of free disk space can be also be gathered without too much fuss, using one of a number of NT command-line utilities; in the following example script, we use the dir command. We then compare this with a warning limit and a catastrophe limit; if the amount of free space falls below either of these limits on any tested drive, an administrator is informed via email.

First, we import the Net::SMTP module. We then set up a number of variables: an array containing a list of drives to check, the thresholds for the warning and catastrophe limits, and the information required by SMTP (see the Appendix). In addition, we initialize the $status with a title line for the email report and set the flag $sendwarning to 0.

use Net::SMTP;
@drives = ("C","D");
$warnlimit = 20000000;
$catastrophelimit = 10000000;
$smtp_host = 'smtp.coolplace.com';
$script_address = 'drivechecker@coolplace.com';
$send_to = 'Ntadmin@coolplace.com';
$status = "Drive status report for $ENV{'COMPUTERNAME'}\n\n";
$sendwarning = 0;

For each drive to be tested, we run dir and use two regular expressions to extract the amount of free space from the output. dir states the amount of free space as the bottom line of its output and uses commas as a thousands separator. The first regex here extracts the number itself; the second removes the commas. The s and m suffixes on the first expression allow the ^ and $ symbols to be used during matching even though the expression as a whole is treated as one long string.

foreach $drive(@drives)
{
    $dir = ‘dir $drive:\\‘;
    $dir =~ s/.*^\D+((\d|,)+).*/$1/sm;
    $dir =~ s/,//g;

Next, the amount of free space is compared first with the catastrophe limit and then with the warning limit. If either is exceeded, the $sendwarning flag is set. In any case, a line describing the outcome of the comparison is appended to $status.

$status.
      if($dir <= $catastrophelimit)
      {
          $status = "${status}Drive $drive has reached its limit\n";
          $sendwarning = 1;
      }
      elsif($dir <= $warnlimit)
      {
          $status = "${status}Drive $drive is reaching its limit\n";
          $sendwarning = 1;
      }
      else
     {
          $status = "${status}Drive $drive has plenty of space\n";
     }
 }

Finally, if the $setwarning flag has been set (i.e., one of the drives has exceeded one of the limits), the administrator is sent an email, the content of which is $status.

if($sendwarning)
{
    $smtp = Net::SMTP->new($smtp_host);
    $smtp->mail($script_address);
    $smtp->to($send_to);
    $smtp->data();
    $smtp->datasend($status);
    $smtp->dataend();
    $smtp->quit;
}

As can be seen, there are two distinct parts to the functionality: first, we check the amount of drive space and compile an information message; next, we send it. The latter part (sending the mail) is common to all scripts in the reporting category.

Reporting Event Log Problems

Checking disk usage is a well-defined problem and therefore lends itself well to scripting. Try to write a script that looks out for hardware failure, driver conflicts, or security alerts and you will get stuck pretty quickly; the problem is far too open ended. Fortunately, many of these sorts of disasters are recorded in the NT event log, and thanks to the Win32::EventLog module, it is relatively easy to write a script that reads the contents of the logs and searches for signs of crisis. As we have seen, it is not a large leap from here to being able to warn an administrator in the form of a suitably alarming email.

All events stored in the log are classified as information, errors, warnings, success audit, or failure audit (see Chapter 5, Controlling Services and Drivers, for more information); the latter two apply only to the security log. Broadly speaking, administrative action is required only for entries in the error, warning, or failure audit categories. The following script parses the event log on the workstation on which it is running, searching for errors and warnings (we ignore audit failures here for the sake of brevity). If it finds any, it reports that fact to an administrator via email.

First, we import the two required modules and initialize an array with the names of the three event logs. Then, we open each log in turn and read it from the top, sequentially (the details of parameters passed to the Read method are described fully in the Appendix).

use Win32::EventLog;
use Net::SMTP;
@eventlogs = ("system","security","application");

foreach $logtype(@eventlogs)
{
    $evtlog = Win32::EventLog->new($logtype, $ENV{COMPUTERNAME});
    while ($evtlog->Read
     (EVENTLOG_FORWARDS_READ | EVENTLOG_SEQUENTIAL_READ,0,\%eventinfo))

For each entry in the log, we detect whether it is an error or a warning, by applying a bit mask to $eventinfo{EventType} (see the Appendix). In either case, we append the name of the event source (the program that logged the event) and the log in which the error was found to a string.

{
          if ($eventinfo{'EventType'} & 3) #it's an error/warning
          {
             if($eventinfo{'EventType'} & 1) #an error
             {
                $errors = "${errors}$eventinfo{'Source'}($logtype log)\n";
             }
             else #a warning
             {
                $warnings = "${warnings}$eventinfo{'Source'}
                              ($logtype log)\n";
             }
          }
     }
}

You may have noticed that if the logs do not contain any errors or warnings, the strings $errors and $warning will not have been created. Next, we use this fact to decide whether an administrator needs to be warned. If either of the strings exists, we need to send mail, so the script constructs a message. Finally, the message is sent using SMTP. The code for this is identical to that for sending disk usage warnings (discussed earlier). For the sake of variety, however, here the information needed for SMTP to work is coded directly where it is required; in the previous example, it was passed as a set of variables.

if($errors || $warnings)
{
    if($errors)
    {$errors = "Errors were logged by the following sources:\n\n$errors";}
    if($warnings)
    {$warnings = "Warnings were logged by the following sources:
                  \n\n$warnings";}
    $message = "Event log report for $ENV{COMPUTERNAME}\n";
    $message = "${message}------------------------------\n";
    $message = "${message}\n${errors}\n${warnings}\n";
    $message = "${message}Please check the event logs for details\n";

    $smtp = Net::SMTP->new('smtp.scripters.com');
    $smtp->mail('eventlogger@scripters.com');
    $smtp->to('ntadmin@scripters.com');
    $smtp->data($message);
    $smtp->dataend();
    $smtp->quit;
}

Typical email output from this script looks very much like this:

Event log report for KANCHENJUNGA
------------------------------

Errors were logged by the following sources:

Print(system log)
Print(system log)

Warnings were logged by the following sources:

Netlogon(system log)
Security(security log)

Please check the event logs for details

Having received such an email, the administrator should use the NT Event Viewer program to read the details of the error or warning (this can be achieved remotely); she will then be able to decide whether it is necessary to take further action. An obvious question to ask at this point is “Can't the script send out the full information as seen in the event viewer?” Well, the answer is “Yes, but it's very tricky indeed unless you happen to be a seasoned programmer, as making sense of the data involves serious and direct use of the Windows API. Hopefully, full support for this will be available within Perl soon.”*

Note that events remain in the event log until they are explicitly purged (or until the log is full). Therefore, if this script is run too regularly, the poor administrator will be bombarded with duplicate information about failing workstations. One way to prevent this would be to modify the script so that it keeps a log of what it has already sent and doesn't duplicate information. However, a much more satisfactory solution would be to combine this script with that for archiving and clearing the log (see the earlier discussion). In this case, the administrator knows that the email he has been sent contains only new information; if duplication occurs, there must be a really persistent problem out there!

Summary

In this chapter we have tried to demonstrate how Perl scripts can be used to automate workstation maintenance tasks that would otherwise have to be carried out by an administrator. Conceptually, we have divided these tasks into two categories: housekeeping and reporting. The former category is concerned with preventing potential problems from occurring, the latter with alerting an administrator's attention to a workstation problem before it becomes serious. We provided four examples of scripts that carried out maintenance tasks relevant to most workstation environments; between them, they illustrate a number of different approaches to scripting automatic maintenance solutions. Over the course of the next few chapters, we show how Perl can be used to carry out major configuration changes on workstations.

*Since Perl is a powerful programming language and can interface with any native library functions (with the help of a native-code module), anything at all that is possible to program within Windows is also possible within Perl. However, the fact that something is possible does not make it practicable. If, for example, you want to change the default behavior of the mouse pointer on all your workstations, Perl is probably not the tool to use unless someone has already written a module to do this!

*The del command will not delete open files—this is a feature, not a bug, as you hardly ever want to swipe an open file from under the nostrils of the application that is using it! It is also worth noting that instead of using NT's del command, you can use Perl's built-in unlink() function here. We favor the NT command in this case, however, because unlink is hardly an intuitive term for non-Unix users; if you are scripting something as potentially serious as file deletion, the script should be made as transparent as possible.

*An alternative approach to deleting files recursively would be to use one (or both) of the Perl modules File::Find and File::Recurse.

*If you are seriously paranoid, create a REG_DWORD value named CrashOnAuditFail under the registry key HKLM\SYSTEM\CurrentControlSet\Control\Lsa; set its value to 1. If the Security log is unable to write audit information, it halts the system with a blue screen of death—clearly, this is often not a desirable course of action, but does prevent unlogged cracking!!

*$ENV{COMPUTERNAME} refers to the NT %computemame% environment variable.

*This module is discussed in detail in the Appendix.

*Constructing a coherent message involves merging the “strings” returned by the Event Viewer with a message text extracted from a compiled binary. The usual way to do this within Windows would be to find out in which module the message is stored (this can be read from the event log section of the registry), loading it (using the LoadLibrary API call), and then calling the FornatMessage API call to retrieve and construct the relevant string. For more information about accessing the NT event logs, see Windows NT Event Logging, by James D. Murray (O'Reilly & Associates, 1998).

Get Windows NT Workstation: Configuration and Maintenance now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.