techrobatics.com

Sysadmin Ex Machina

Migration Complete

No comments

I’ve moved the site from Slicehost to Linode. Everything appears to be working. I’ll have more details about the decision later. For now, let’s just all be glad it works. :)

In this example, I try something a bit more complicated. This was originally done in Ruby, as an exercise for an interview, but I’ve decided to make it a bake-off post to follow up from last month. What follows is mostly a linguistic analysis. In the coming months, I’ll start doing performance and reliability comparisons as well.

The Test
Working with only the standard libraries for Python and Ruby, I read in three files, each delimited differently, and each with different column orders and date formats. The goal: parse the data, and combine it all into a single array-of-arrays, and provide a mechanism for printing formatted output to the console.

Here are the input data specifics:

You will be given 3 files, each containing records stored in a different format.

The pipe-delimited file lists each record as follows:
LastName | FirstName | MiddleInitial | Gender | FavoriteColor | DateOfBirth

The comma-delimited file looks like this:
LastName, FirstName, Gender, FavoriteColor, DateOfBirth

And lastly, the space-delimited file:
LastName FirstName MiddleInitial Gender DateOfBirth FavoriteColor

You may assume that the delimiters (commas, pipes and spaces) do not appear anywhere in the data values themselves. Write a program to read in records from these files and combine them into a single set of records.

And here is what the console output should look like:

An output record consists of the following 5 fields: last name, first name, gender, date of birth and favorite color.

o Output 1 – sorted by gender (females before males) then by last name ascending.
o Output 2 – sorted by birth date, ascending.
o Output 3 – sorted by last name, descending.
* Ensure that fields are displayed in the following order: last name, first name, gender, date of birth, favorite color.
* Display dates in the format MM/DD/YYYY.

My Thinking
I decided to write this as a rudimentary class library, to see how each language handles object orientation. Also, I decided to include a Perl version of the project, just for kicks, and to provide a “reference” comparison of the functionality of the newer languages with something a little more mature (I’ll spare y’all the REXX version).

First step in the process, was to decide what the objects were, and what I needed to do to them. In a nutshell, the exercise is really just asking for an array object, containing all the data from the three files, merged into one list. The things we need to do to the list are: reorganize the columns, sort the rows, and reformat the date strings.

But not all of these things need to be methods. Printing the data in various ways could just be done in an application (or script), that uses the object. But I didn’t have this in mind when I started. You’ll see that the Ruby version has a sort, a print(format), and various forms of get methods. The Python version, which I wrote next, also has the basic get, a sort, and a print(format) methods. But with the Perl version, I pared the class down to nothing more than a get and a sort method, and did my print layout in the execution script.

So, the basic class would look like:

Class: DataParser
Method: get
Method: sort
Method: print (sometimes omitted)

Notes:
It turns out, there are a few bits and bobs missing from each language (as I expected would be the case). First, is that I couldn’t figure out how to tell the date formatters to use slashes instead of dashes, on the returned dates. So, in all three, I had to roll-my-own. Second, was discovering that, even after all these years, Perl still has no trim (“strip”) method on strings. That’s just bizarre. :| Again, I had to roll my own.

Classes and Methods
One of the more confounding dialectic differences, is the way in which code blocks are bundled in each language. To me, Python has the most straightforward: Package, Module, Class, Method/Function. I used to complain about the strict spacing requirement in Python, but I’ve actually found it to be a helpful tool, when trying to read my code. How do I know which definitions are methods and which are functions? If they are indented under the current class, they are methods owned by it. What’s more, just about anything can be easily imported into any other module in Python, just by including it in your pythonpath, and importing the specific classes you want. With Ruby and Perl, getting things recognized was a bit of a chore.

File Handling
A feature I really appreciated in both Python and Perl, was the ability to abstract directory lookups and filespec construction. This makes it easy to port those scripts to other (non-*nix) environments. This is probably possible in Ruby as well. I just haven’t had the time to comb over the Ruby version with the best ideas I culled from Python and Perl.

Data Manipulation
Hands-down, the easiest language with which to build and sort arrays was Ruby. The selection of methods available for manipulating strings and tabular data in Ruby is breathtaking! Python and Perl, by comparison, were quite a bit more difficult because of the esoterics necessary to make the sorts work the way I expected them to (lambdas in Python, and the need for nested comparisons in Perl).

Python and Ruby standard libraries come equipped with a lot of intuitively easy to use methods for which the analogous functionality in Perl requires a lot of deciphering. Also, a lot of building from scratch. So, I gave up and decided to cheat on the Perl version – I used a few of the most commonly accepted add-ons for Perl. One of those, was a library that dynamically identified the field separator in CSV files. While it was easy to use, and put little extra burden on the script as a whole, you can see that it didn’t save me much typing, when compared to the Python version of the script. In hindsight, I probably could have done a grep-like lookup the way I did in the Python version.

The other thing the Perl version taught me, was that I probably didn’t need a separate date formatting function to accomplish what I was trying to do with the raw data. As you can see there, with no method calls at all (except for basic string manipulation), I was able to get the date format I wanted in one small line of code.

Comments, Doc, and Testing
One of the nicest features of Python, is the inclusion of a doc generator from docstrings. Ruby and Perl offer the same as add-ons, but it came out of the box with my instance of Python 2.6.4. With both Python and Ruby, setting up unit tests for these classes was also a snap, as both languages offer it out of the box as well. With Perl, setting that up was a good deal more complicated.

And, while I’m on the subject, I did indeed include a set of unit tests in my experiment, for both the python and the ruby version. But I’m not going to address those here, as I want to do a full separate post on testing, later.

The (Somewhat) Finished Products
So, without further ado, here is the code —

First, Ruby:

###
# REQUIRED
###
require 'date'

###
# FUNCTIONS
###
def format_date(date)
  year, month, day = date.split(/-/) # splits date into 3 variables
  month = month.sub(/^0/,'') # Strips leading '0's from the month
  return month + "/" + day + "/" + year # concatenates and returns date
end

module CSV_Processor 

	class DataParser
		attr_accessor :input_dir, :file_mask
		def initialize(input_dir, file_mask)
			@all_records = []
			@input_dir = input_dir
			@file_mask = file_mask

			Dir[@input_dir + "/" + @file_mask].each {|fname|
		    case
		      when fname.match('comma') then sep = ','
		      when fname.match('space') then sep = ' '
		      when fname.match('pipe') then sep = '|'
		      #in case a stray file enters the data directory
		      else raise "Unable to identify delimiter for " + fname
		    end

			#In case we can't open the file.
			begin
			  f = File.open(fname,'r')
			rescue Exception => e
			  puts e.message
			  exit
			end

			f.each_line {|row|
				fields = row.split(sep).collect {|x| x.chomp().strip}
			    case
			      when fname.match('comma')
			      	last, first, gender, color, dobraw = fields
					dob = format_date(Date.strptime(dobraw,"%m/%d/%Y").to_s)
			      when fname.match('space')
					last, first, unused, gender, dobraw, color = fields
					dob = format_date(Date.strptime(dobraw,"%m-%d-%Y").to_s)
			      when fname.match('pipe')
					last, first, unused, gender, color, dobraw = fields
					dob = format_date(Date.strptime(dobraw,"%m-%d-%Y").to_s)
				  else raise "Invalid File Type."
			    end #case

			    gender = 'Male' if gender == 'M'
			    gender = 'Female' if gender == 'F'

			    out_record = [last,first,gender,dob,color]
			    @all_records.push(out_record)

			    }#each line
			} #foreach file
		end#initialize

		def sort_records(sort_type)
			case
				when sort_type == 1 then @all_records.sort_by{|e| [e[2],e[0]]}
				when sort_type == 2 then @all_records.sort_by{|e| [e[3].split('/')[2]]}
				when sort_type == 3 then @all_records.sort_by{|e| [e[0]]}.reverse!
				else raise "Invalid Sort Type."
			end#case
		end#record_sort

		def format_records(sort_type)
			print_records = []
			print_records << "Last, First     \t Gender      \t Date of Birth \t Favorite Color \n"
			print_records << "-----------     \t ----------- \t ------------- \t -------------- \n"
			sort_records(sort_type).each {|line|
					print_records << "#{line[0]}, #{line[1]}      \t #{line[2]}      \t #{line[3]} \t #{line[4]} \n"
			}
			print_records.to_s
		end

		def to_array()
		  @all_records
		end

		def to_s()
			# The raw accumulated total of all records in all files, without sorting,
			# But with data formatting.
			@all_records.to_s
		end#to_s

		def dir_to_s()
		  @input_dir + "/" + @file_mask
		end

	end#class

end#module

And next, the Python version:

###
# REQUIRED / IMPORTS
###
import os, glob                   #needed for the directory listing
from datetime import datetime     #needed for the date parsing/formatting
import re                         #regex, needed for the delimiter parsing

###
# FUNCTIONS
###
def format_date(datestr,fmtstr):
    """format_date: reconfigure the normal dash syntax date formatting, to
    slash syntax, and reorder the elements to conform to the exercise requirements

    @param datestr: the input datestr from the data file

    @param fmtstr: the format
    """
    pydate = str(datetime.strptime(datestr, fmtstr)).split()[0] #python adds the time
    datechunks = pydate.split("-")
    return datechunks[1] + "/" + datechunks[2] + "/" + datechunks[0]

class DataParser(object):
    """DataParser: Parses and outputs data from various delimited data files."""
    def __init__(self, input_dir, file_mask):
        self._input_dir = input_dir
        self._file_mask = file_mask
        self._all_records = []

        for infile in glob.glob(os.path.join(self._input_dir, self._file_mask)):
            #couldn't quite figure out a way to make this a single block
            #(rather than three separate if/elifs. But you can see the split is
            #generalized already, so if anyone can come up with a better way,
            #I'm all ears!!
            for row in open(infile,'r').readlines():
                if infile.find('comma') > -1:
                    datefmt = "%m/%d/%Y"
                    last, first, gender, color, dobraw = \
                            [x.strip() for x in re.split(r'[ ,|;"\t]+', row)]
                elif infile.find('space') > -1:
                    datefmt = "%m-%d-%Y"
                    last, first, unused, gender, dobraw, color = \
                            [x.strip() for x in re.split(r'[ ,|;"\t]+', row)]
                elif infile.find('pipe') > -1:
                    datefmt = "%m-%d-%Y"
                    last, first, unused, gender, color, dobraw = \
                            [x.strip() for x in re.split(r'[ ,|;"\t]+', row)]
                    #There is also a way to do this with csv.Sniffer, but the
                    #spaces around the pipe delimiter also confuse sniffer, so
                    #I couldn't use it.
                else: raise ValueError(infile + "is not an acceptable input file.")

                dob = format_date(dobraw,datefmt)
                if gender == 'M': gender = 'Male'
                if gender == 'F': gender = 'Female'

                self._all_records.append([last,first,gender,dob,color])

    def get_records(self):
        return self._all_records

    def sort_records(self,sort_type):
        self._sort_type = sort_type
        #By gender ascending, then by last-name ascending, using key sort
        #Unlike Ruby, this can be extended to sort every element hierarchically
        #I stopped at sorting by gender and name, to meet the exercise requirements.
        if sort_type == 1: self._all_records.sort(key=lambda row: (row[2],row[0]))
        #By date-of-birth ascending, using cmp sort
        elif sort_type == 2: self._all_records.sort(cmp=lambda x,y: cmp(x[3].split('/')[2], y[3].split('/')[2]))
        #By last-name descending, using reverse parm instead of reverse method, for efficiency
        elif sort_type == 3: self._all_records.sort(reverse=True)
        else: raise ValueError("Invalid Sort Type")
        return self._all_records

    def format_records(self,sort_type):
        self._print_records = []
        self._print_records.append("Last, First         \tGender      \tDate of Birth \tFavorite Color")
        self._print_records.append("-----------         \t----------- \t------------- \t--------------")
        for record in self.sort_records(sort_type):
            self._print_records.append(record[0] + ", " + record[1] + "      \t" + record[2] + "\t\t" + record[3] + "\t" + record[4])
        return self._print_records

And, for a bonus round, here’s the entire thing rewritten in Perl:

#!/usr/bin/perl -w
package DataParser;
	use autodie;
	use File::Spec;
	use Text::CSV_XS;
	use Text::CSV::Separator qw(get_separator);
	sub new {
	    my $class = shift;
	    my $self = {
	        _input_dir => shift,
	        _file_mask  => shift,
	       };

	    $self->{ALL_RECORDS}  = []; #the output array

	    # get the directory file list
	    opendir(my $dh, $self->{_input_dir}); #relies on autodie
		my @filelist = grep {$_ ne '.' && $_ ne '..'} readdir $dh;
		closedir $dh;
	    # print each file in the directory
	    foreach $file (@filelist){
			my $fullfile = File::Spec->catfile( $self->{_input_dir}, $file );

			#Identify separators
	        my @chars = get_separator(    path => $fullfile,
	        						   include => [" ","|",","],);
	        my $sep;
	        if (@chars) {
    			if (@chars == 1) {$sep = $chars[0];} #first character in array
    			else {$sep  = $chars[1];} #second character in array
			} else { die "Couldn't detect the field separator in $fullfile: $!\n";}
	        my $csv=Text::CSV_XS->new({ sep_char => $sep });

	        #parse the file, and generate the new array
	        open(my $fh,'<',$fullfile); #relies on autodie
            while (<$fh>){
                $csv->parse($_);
				my @columns = $csv->fields();
				$last_name = trim($columns[0]);
				$first_name = trim($columns[1]);
                if ($sep eq " "){
                	$middle = trim($columns[2]);
                	$gender = trim($columns[3]);
                	$dob = join('/', split(/-/, trim($columns[4])));
                	$color = trim($columns[5]);
                } elsif ($sep eq "|"){
                	$middle = trim($columns[2]);
                	$gender = trim($columns[3]);
                	$color = trim($columns[4]);
                	$dob = join('/', split(/-/, trim($columns[5])));
                } elsif ($sep eq ","){
                	$gender = trim($columns[2]);
                	$color = trim($columns[3]);
                	$dob = join('/', split(/\//, trim($columns[4])));
                }
                else{ die "Couldn't identify data columns in $fullfile: $!\n"}

				if ($gender eq 'M'){$gender='Male'}
				if ($gender eq 'F'){$gender='Female'}

                @newrow = [$last_name,$first_name,$gender,$color,$dob];
				push(@{$self->{ALL_RECORDS}},@newrow);
                }
            close $fh;
            }
	bless ($self,$class);
	return $self;
    }

    sub get_records {
		my $self = shift;
    	return @{ $self->{ALL_RECORDS} };
    }

    sub sort_records {
    	my $self= shift;
    	my $sort_type = shift;
    	my @sorted_records;
    	if ($sort_type == 1) {
    		#By gender ascending, and then by last-name ascending.
    		for $record ( sort {($a->[2] cmp $b->[2]) || ($a->[0] cmp $b->[0])}
    			@{ $self->{ALL_RECORDS} } ) {
    				push(@sorted_records,$record);
    			}
    		}
    	elsif ($sort_type == 2) {
    		#By date-of-birth ascending
    		for $record ( sort {substr($a->[4],5,4) cmp substr($b->[4],5,4)}
	    		@{ $self->{ALL_RECORDS} } ) {
    				push(@sorted_records,$record);
				};
    		}
    	elsif ($sort_type == 3) {
    		#by last-name descending
	    	for $record ( reverse sort { $a->[0] cmp $b->[0] }
	    		@{ $self->{ALL_RECORDS} } ) {
    				push(@sorted_records,$record);
				};
    		}
    	else {die "Sort Type $sort_type Not Valid: $!";};
        return @sorted_records;
    	}

    # Shockingly, Perl still has no builtin method for this.
	sub trim($){
		my $string = shift;
		$string =~ s/^\s+//;#trim the front
		$string =~ s/\s+$//;#trim the back
		return $string;
		}

###############################################################################
# SHORT SCRIPT TO INSTANTIATE THE OBJECT, AND MANIPULATE THE DATA
###############################################################################
$dp = new DataParser("Data", "*.txt");

@recds = $dp->get_records();
$recds = @recds;
print "\nTotal Unsorted Records: ",$recds,"\n";

@sort_types = (1,2,3);
foreach $type (@sort_types){
	print "\n--------------> SORT TYPE [",$type,"] <----------------------\n"; 	print "Last, First     \tGender \tColor \tBirthday\n"; 	print "------------------\t------\t------\t-------------\n"; 	@sortrecs = $dp->sort_records($type);
	foreach(@sortrecs){
		print $_->[0], ", ", $_->[1],"      \t", $_->[2], "\t", $_->[3], "\t", $_->[4], "\n";
		}
	}

I’m experimenting with comparative differences in Python and Ruby. Here is an example from each language, designed to do exactly the same thing: go and get a copy of “A Comedy of Errors” from Gutenberg, and dump it to the console. Not too useful, but interesting from the perspective of seeing how the two languages handle interacting with the web (the one thing they both claim to do extremely well).

First, the ruby version:

module HTTP_Examples
  require 'net/http'
  require 'uri'

  class SimpleTesting
    def initialize()
    end

    def fetch(uri_str, limit = 10)
      raise ArgumentError, 'HTTP redirect too deep' if limit == 0

      response = Net::HTTP.get_response(URI.parse(uri_str))
      case response
        when Net::HTTPSuccess     then response
        when Net::HTTPRedirection then fetch(response['location'], limit - 1)
        else response.error!
      end
    end
  end

  test = SimpleTesting.new()

  response = test.fetch('http://www.gutenberg.org/cache/epub/2239/pg2239.txt')
  print response.body
end

The second version is Python:

from urllib2 import URLError, urlopen, Request

class SimpleTesting(object):

    def fetch(self, uri_str, limit=10):
        if limit == 0:
            raise ValueError,'HTTP redirect too deep'
        try:
            response = urlopen(Request(uri_str))
        except URLError, e:
            if hasattr(e, 'code'):
                if e.code == 301:
                    print e.code, e.reason
                    self.fetch('http://www.gutenberg.org/')
        else: #everything is fine
            return response

def main():
    test = SimpleTesting()
    response = test.fetch('http://www.gutenberg.org/cache/epub/2239/pg2239.txt')
    print response.read()

if __name__ == "__main__":
    main()

In general, the two examples aren’t all that starkly different. But there are some significant divergences.

First, the Ruby version makes library calls in much the same way as Perl does, while the Python version conforms more to the Java model.

Second, Python doesn’t seem to have an analogy to CASE, so you have to wrap everything in an exception handler. But with HTTP, some of the responses aren’t really “exceptions” so much as simply higher-level responses than 200. Why?

Third, is that Python seems to be able to deal with redirection dynamically, while Ruby needs to recursively iterate through each new location. Why?

Lastly, Ruby is much easier on the eyes, and easier on the brain, I think. Aside from the ugly Perl-like library calls, there’s no need to define and then superflously call any of that junk called “main” (as is a convention in java as well, I believe), in Ruby. Again, why?

These, and many other questions, will be addressed as my exploration continues…

Did this one just for the fun of it. Please feel free to use it as you see fit.

#!/usr/bin/env python

import urllib2
import random
import string

class RandomItems(object):
    """This is the root class for the randomizer subclasses. These
        are used to generate arbitrary content for each of the fields
        in a csv file data row. The purpose is to automatically generate
        content that can be used to exercise sse functionality
        automatically through the sse autotester.
    """
    def __iter__(self):
        while True:
            yield self.next()

    def slice(self, times):
        return itertools.islice(self, times)

class RandomWords(RandomItems):
    """Obtain a list of random real words from the internet, place them
        in an iterable list object, and provide a method for retrieving
        a subset of length 1-n, of random words from the root list.
    """
    def __init__(self):
        urls = [
            "http://dictionary-thesaurus.com/wordlists/Nouns%285,449%29.txt",
            "http://dictionary-thesaurus.com/wordlists/Verbs%284,874%29.txt",
            "http://dictionary-thesaurus.com/wordlists/Adjectives%2850%29.txt",
            "http://dictionary-thesaurus.com/wordlists/Adjectives%28929%29.txt",
            "http://dictionary-thesaurus.com/wordlists/DescriptiveActionWords%2835%29.txt",
            "http://dictionary-thesaurus.com/wordlists/WordsThatDescribe%2886%29.txt",
            "http://dictionary-thesaurus.com/wordlists/DescriptiveWords%2886%29.txt",
            "http://dictionary-thesaurus.com/wordlists/WordsFunToUse%28100%29.txt",
            "http://dictionary-thesaurus.com/wordlists/Materials%2847%29.txt",
            "http://dictionary-thesaurus.com/wordlists/NewsSubjects%28197%29.txt",
            "http://dictionary-thesaurus.com/wordlists/Skills%28341%29.txt",
            "http://dictionary-thesaurus.com/wordlists/TechnicalManualWords%281495%29.txt",
            "http://dictionary-thesaurus.com/wordlists/GRE_WordList%281264%29.txt"
        ]
        self._words = []
        for url in urls:
            urlresp = urllib2.urlopen(urllib2.Request(url))
            self._words.extend([word for word in urlresp.read().split("\r\n")])
        self._words = list(set(self._words)) # Remove duplicates
        self._words.sort() # sort the list

    def next(self):
        """Return a single random word from the list
        """
        return random.choice(self._words)

    def get(self):
        return self._words

    def wordcount(self):
        return len(self._words)

    def sublist(self,size=3):
        segment = []
        for i in range(size):
            segment.append(self.next())
        #printable = " ".join(segment)
        return segment

    def random_name(self):
        words = self.sublist()
        return "%s %s %s" % (words[0], words[1], words[2])

def main():
    wl = RandomWords()
    print wl.wordcount()
    print wl.next()
    print wl.sublist()
    print 'My Class Name = %s' % wl.random_name()
    print wl.get()

if __name__ == "__main__":
    main()

I’ve been working on a project for a friend of mine, over the last couple weeks. He’s using a Windows 2003 Server, with IIS 6.

I’m not uncomfortable working from a Windows command shell, but my own personal preference is bash. He’s also got a tight budget for bandwidth, so Cygwin is the way to go for SSH and SFTP.

What’s handy about the Cygwin solution, is that VBScript is available to me as a console scripting service, which gives me access to the entire WMI infrastructure through the Cygwin command line. Sweet.

Anyway, I’ve coded up a bunch of handy utilities for myself, that makes it so that I don’t have to RDP in every time I want to look at the logs, or do basic system checks.

The first, is for reading the Windows Event logs. You can change the selectedLog to your preference, or you can modify the script to take parms. I was considering rewriting this to provide a different layout for the console output for each of the different logs (System, Application, Security):

selectedLog = "System"

strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
    & "{impersonationLevel=impersonate}!\\" _
    & strComputer & "\root\cimv2")
Set colLoggedEvents = objWMIService.ExecQuery _
    ("Select * from Win32_NTLogEvent " _
        & "Where Logfile = '" & selectedLog & "'")
For Each objEvent in colLoggedEvents
	pos = Instr(objEvent.TimeWritten,".") - 1
	evFullDate = Left(objEvent.TimeWritten,pos)
	evDate = Left(evFullDate,8)
	evTime = Right(evFullDate,6)
	'wscript.echo objEvent.Type & " " & Len(objEvent.Type)
	if Len(objEvent.Type) = 11 then
		evSev = objEvent.Type ' & " "
	else
		'pad out severity to 11 bytes, plus a space
		evSev = objEvent.Type & Space((11-len(objEvent.Type)) )
	end If
	Wscript.Echo _
		objEvent.RecordNumber & " " & evDate & " " & evTime & " " & _
		evSev & " " & Trim(objEvent.User) & " " & objEvent.SourceName & _
		" " & objEvent.Message

Next

The next script lists the IIS virtual host log directory for a specified site. I use it to tell me when the log directory reaches a critical mass, and to generate a Table of Contents for a rar file which gets pushed off to Amazon S3 periodically. Even if you don’t need it for IIS, it’s still cool for all the nifty little functions in it:

'set constant vars ************************************************************
'* All of these variables could be passed as command-line parameters
'******************************************************************************
strLocalhost = "localhost"          'We only care about what's on this box
strSiteID = "xxxxxxxxx"             'IIS ID Number for "Some Virtual Hosted Site"
strServerMetaType = "W3SVC"         'We only care about W3 services
strServerType = "Web"               'We only care about Web servers
'******************************************************************************
'create objects
dim oFS, oFolder, objServer
Set objService = GetObject( "IIS://" & strLocalhost & "/" & strServerMetaType )
set oFS = WScript.CreateObject("Scripting.FileSystemObject")

'get site dependent variables
For Each  objServer in objService
    If objServer.Class = "IIs" & strServerType & "Server" _
    AND objServer.Name = strSiteID THEN
        webSiteName = objServer.ServerComment
        webSiteInstance = objServer.Name
        webLogRoot = objServer.LogFileDirectory
    End If
Next
webSiteLogDir = webLogRoot & "\" & strServerMetaType & webSiteInstance

set oFolder = oFS.GetFolder(webSiteLogDir)
ShowFolderDetails oFolder
CreateToc webSiteLogDir

'------------------------------------
' SUBROUTINES AND FUNCTIONS
'------------------------------------
sub ShowFolderDetails(oF)
    dim F
    If (oF.Name = strServerMetaType & webSiteInstance) _
    AND (int(of.Size/1000000000) >= 1) then
        wscript.echo _
        "IIS Site Name:" & vbTab & webSiteName & vbCrLF & _
        "Log Directory:" & vbTab & oF.Name & vbCrLF & _
        "Directory Size:" & vbTab & int(of.Size/1000000000) & "GB" & vbCrLF &_
        "File Count:" & vbTab & of.Files.Count
        wscript.echo "------------------------"
        for each strFile in of.Files
            set objFile = oFS.GetFile(strfile)
            wscript.echo strFile & "  " & int(strFile.size/1000000) & "MB" &_
            vbTab & objFile.DateCreated
        next
    End If
    'wscript.echo vbTab & "Subdirectories:" & vbTab & oF.Subfolders.count
    for each F in oF.Subfolders
        ShowFolderDetails(F)
    next
end sub

sub CreateTOC(sFolder)
  Dim fso, folder, files, NewsFile
  Set fso = CreateObject("Scripting.FileSystemObject")
  'sFolder = Wscript.Arguments.Item(0)
  If sFolder = "" Then sFolder = webLogRoot
  archiveTOC = "TOC-" & webSiteInstance & "-" & CustomDate(Now()) & ".txt"
  Set NewFile = fso.CreateTextFile(sFolder & "\archive\" & archiveTOC, True)
  Set folder = fso.GetFolder(sFolder)
  Set files = folder.Files

  For each folderIdx In files
    'wscript.echo folderIdx
    NewFile.WriteLine(folderIdx)
  Next
  NewFile.Close
end sub

Function CustomDate(dt)
    if dt = "" then dt = Now()
    yr = Year(dt)
    mt = PadDigits(Month(dt),2)
    dy = PadDigits(Day(dt),2)
    hr = PadDigits(Hour(TimeValue(Now())),2)
    mn = PadDigits(Minute(TimeValue(Now())),2)
    sc = PadDigits(Second(TimeValue(Now())),2)
    CustomDate = yr & mt & dy & "_" & hr & mn & sc
End Function

Function PadDigits(n, totalDigits)
    PadDigits = Right(String(totalDigits,"0") & n, totalDigits)
End Function

On the heals of that, there’s also this script, which I use to just dump IIS virtual host info (its how I got the IIS Site ID above. It’s not SUPER useful, but does tell me when certain virtual hosts have been activated or deactivated silently, and its a good way to confirm that IIS is responding:

OPTION EXPLICIT

DIM strServer, strServerType, strServerMetaType
DIM objService

strServer = "localhost"
strServerType = "Web"
strServerMetaType = "W3SVC"

IF WScript.Arguments.Length >= 1 THEN
    strServer = WScript.Arguments( 0 )
END IF

IF WScript.Arguments.Length = 2 THEN
    strServerType = WScript.Arguments( 1 )

    IF UCASE( strServerType ) = "FTP" THEN
        strServerType = "Ftp"
        strServerMetaType = "MSFTPSVC"
    ELSE
        strServerType = "Web"
        strServerMetaType = "W3SVC"
    END IF
END IF

WScript.Echo "Enumerating " & strServerType & "sites on " & strServer & VbCrLf
SET objService = GetObject( "IIS://" & strServer & "/" & strServerMetaType )
EnumServersites objService

SUB EnumServersites( objService )
    DIM objServer, strBindings

    FOR EACH objServer IN objService
        IF objServer.Class = "IIs" & strServerType & "Server" THEN
            WScript.Echo _
                "Site ID = " & objServer.Name & VbCrLf & _
                "Comment = """ & objServer.ServerComment & """ " & VbCrLf & _
                "State   = " & State2Desc( objServer.ServerState ) & VbCrLf & _
                "LogDir  = " & objServer.LogFileDirectory & _
                ""

            ' Enumerate the HTTP bindings (ServerBindings) and
            ' SSL bindings (SecureBindings) for HTTPS only
            strBindings = EnumBindings( objServer.ServerBindings )

            IF strServerType = "Web" THEN
                strBindings = strBindings & _
                EnumBindings( objServer.SecureBindings )
            END IF

            IF NOT strBindings = "" THEN
                WScript.Echo "IP Address" & VbTab & _
                             "Port" & VbTab & _
                             "Host" & VbCrLf & _
                             strBindings
            END IF
        END IF
    NEXT

END SUB

FUNCTION EnumBindings( objBindingList )
    DIM i, strIP, strPort, strHost
    DIM reBinding, reMatch, reMatches
    SET reBinding = NEW RegExp
    reBinding.Pattern = "([^:]*):([^:]*):(.*)"

    FOR i = LBOUND( objBindingList ) TO UBOUND( objBindingList )
        ' objBindingList( i ) is a string looking like IP:Port:Host
        SET reMatches = reBinding.Execute( objBindingList( i ) )
        FOR EACH reMatch in reMatches
            strIP = reMatch.SubMatches( 0 )
            strPort = reMatch.SubMatches( 1 )
            strHost = reMatch.SubMatches( 2 )

            ' Do some pretty processing
            IF strIP = "" THEN strIP = "All Unassigned"
            IF strHost = "" THEN strHost = "*"
            IF LEN( strIP ) < 8 THEN strIP = strIP & VbTab

            EnumBindings = EnumBindings & _
                           strIP & VbTab & _
                           strPort & VbTab & _
                           strHost & VbTab & _
                           ""
        NEXT

        EnumBindings = EnumBindings & VbCrLf
    NEXT

END FUNCTION

FUNCTION State2Desc( nState )
    SELECT CASE nState
    CASE 1
        State2Desc = "Starting (MD_SERVER_STATE_STARTING)"
    CASE 2
        State2Desc = "Started (MD_SERVER_STATE_STARTED)"
    CASE 3
        State2Desc = "Stopping (MD_SERVER_STATE_STOPPING)"
    CASE 4
        State2Desc = "Stopped (MD_SERVER_STATE_STOPPED)"
    CASE 5
        State2Desc = "Pausing (MD_SERVER_STATE_PAUSING)"
    CASE 6
        State2Desc = "Paused (MD_SERVER_STATE_PAUSED)"
    CASE 7
        State2Desc = "Continuing (MD_SERVER_STATE_CONTINUING)"
    CASE ELSE
        State2Desc = "Unknown state"
    END SELECT

END FUNCTION

Lastly, for this post, is a little script I wrote to run on a Scheduled Timer, which tells me when my friend's C: drive drops below a comfortable threshold. Maybe you'll find it helpful:

Const HARD_DISK = 3
strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
    & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")

'Get the hostname
Set colCompitems = objWMIService.ExecQuery _
    ("Select * from Win32_ComputerSystem", , 48)
For Each objItem in colCompitems
    strHostname = objItem.Name
Next

'Get the disk information
Set colDisks = objWMIService.ExecQuery _
    ("Select * from Win32_Volume Where DriveType = " & HARD_DISK & "")
For Each objDisk in colDisks
    drivename = objDisk.Label
    remaining = int(objDisk.FreeSpace/1000000000)
    If remaining < 8 then
        subject = "Warning: " & drivename & " Disk is running low on space!"
        body = "There is only " & remaining & _
        "GB of disk space remaining. Disk space will need to be found soon."
        sendNotification subject,body
        errType = "WARNING"
        LogEvent errType,body
    Elseif remaining < 2 then
        subject = "ALERT! " & drivename & " DISK IS CRITICAL!"
        body = drivename & " IS NOW AT " & remaining & "GB." & _
        " SERVER FAILURE IS IMMINENT."
        sendNotification subject,body
        errType = "ERROR"
        LogEvent errType,body
    Else LogEvent "INFORMATION","Disk Capacity Is Acceptable (" & remaining & "GB)"
    End If
Next

sub sendNotification(alertSubject,alertBody)
    Set objEmail = CreateObject("CDO.Message")
    objEmail.From = "FDR-admin-bot@freedomainradio.com"
    objEmail.To = "greg@techrobatics.com,s.molyneux@rogers.com"
    objEmail.Subject = alertSubject
    objEmail.Textbody = alertBody
    objEmail.Send
end sub

'*************
'THIS IS ONLY NEEDED IF SMTP SERVICE EVER GOES OUTBOARD
'*************
sub sendRelayNotification(alertSubject,alertBody)
    Set objEmail = CreateObject("CDO.Message")
    objEmail.From = "FDR-admin-bot@freedomainradio.com"
    objEmail.To = "greg@techrobatics.com,s.molyneux@rogers.com"
    objEmail.Subject = alertSubject
    objEmail.Textbody = alertBody
    objEmail.Configuration.Fields.Item _
        ("http://schemas.microsoft.com/cdo/configuration/sendusing") = 2
    objEmail.Configuration.Fields.Item _
        ("http://schemas.microsoft.com/cdo/configuration/smtpserver") = _
            "FDRDuo.secureserver.net"
    objEmail.Configuration.Fields.Item _
        ("http://schemas.microsoft.com/cdo/configuration/smtpserverport") = 25
    objEmail.Configuration.Fields.Update
    objEmail.Send
end sub

sub LogEvent(errLevel,logMessage)
    runThis =  "%COMSPEC% /c eventcreate /s " & strHostname _
        & " /so ""Low Disk Space"" /T " & errLevel & " /ID 1000 /L System /D """ _
        & logMessage & ""
        WindowStyle = 0 'Do not pop up a dos box
        Set WshShell = WScript.CreateObject("WScript.Shell") 'generate the object
        Call WshShell.Run (runThis, WindowStyle, false) '(COMSPEC = cmd.exe)
        Set WshShell = Nothing
end sub

You could move that LogEvent subroutine into a handy-dandy little utility of its own, taking hostname, source, errorlevel, and message as parameters. I'm working on that for myself, too.

That's all for now...