May 19, 2009

DIY ESX Server Health Monitoring - Part 3

Updated: June 18, 2009
Added a semicolon to run-esx-report.sh that was left out and responsible for some ugly HTML formatting.

With the secure SSH access problem solved in Part 2, we'll move on to getting the data in the proper format and emailing it from the ESX Service Console. As you probably know, the Linux distribution installed with ESX 3.5 lacks sendmail or an equivalent command, but we can roll our own from a perl script.

The perl mailer script
We need to import two perl modules for the script, and both are included by default in the Service Console. Getopt::Std provides a simple way to get command line options, and Net::SMTP will interface with an Exchange or SMTP server accessible from the console network:

  use Getopt::Std;
  use Net::SMTP;

This getopt call is all that's necessary to declare the command line options (-f, -r, -s, etc.), and it will automatically populate a set of corresponding variables named opt_*. We'll do a quick check to make sure all the command line options were specified, and if not display the usage message:

  getopt ('frsmb');

  unless ($opt_f && $opt_r && $opt_s && $opt_m && $opt_b) {
    print_usage();
    exit 1;
  }

Next we'll create a filehandle named BODY, opening the file specified on the command line. After reading in each line to the variable body_data, we'll close the handle:

  open(BODY, $opt_b) || error("Could not open file $opt_b.");

  my @body_data=<BODY>;
  close(BODY);

The Net::SMTP module is pretty straightforward. To generate a HTML formatted email, we just need to specify the MIME version, the content type as HTML, and the character encoding as ISO 8859-1. If you would rather send the message as plain text, just remove those two lines:

  my $smtp = Net::SMTP->new($opt_m) ||
    error("SMTP connection to $opt_m failed.");
  $smtp->mail($opt_f);
  $smtp->to($opt_r);
  $smtp->data();
  $smtp->datasend("MIME-Version: 1.0\n");
  $smtp->datasend("Content-Type: text/html; charset=iso-8859-1\n");
  $smtp->datasend("To: $opt_r\n");
  $smtp->datasend("From: $opt_f\n");
  $smtp->datasend("Subject: $opt_s\n");
  foreach $line (@body_data)
    {
      $smtp->datasend("$line");
    }
  $smtp->dataend();
  $smtp->quit;

Here's the complete html-mailer.pl script:


###############################################################################
#
#  html-mailer.pl
#
###############################################################################
#
#  To create the html-mailer.pl script in the ~/esx-report directory, copy
#  this entire code segment into your shell.
#  If you'd rather copy just the script, select everything between the
#  SCRIPTCREATOR limit strings.
#
###############################################################################

# If the ~/esx-report directory exists, cd to it so the script is created there
[ -d ~/esx-report ] && cd ~/esx-report

cat > ./html-mailer.pl <<'SCRIPTCREATOR'
#! /usr/bin/perl -w

 use strict;
 use Getopt::Std;
 use Net::SMTP;

 # Options:
 # $opt_f  email address of the sender
 # $opt_r  recipient email address
 # $opt_s  message subject, enclose in quotes if spaces
 # $opt_m  SMTP server FQDN or IP address
 # $opt_b  HTML formatted file for the message body

  our ($opt_f, $opt_r, $opt_s, $opt_m, $opt_b);

  getopt ('frsmb');

  unless ($opt_f && $opt_r && $opt_s && $opt_m && $opt_b) {
    print_usage();
    exit 1;
  }

  open(BODY, $opt_b) || error("Unable to open file $opt_b");

  my @body_data=<BODY>;
  close(BODY);
  my $line;

  my $smtp = Net::SMTP->new($opt_m) ||
    error("SMTP connection to $opt_m failed");
  $smtp->mail($opt_f);
  $smtp->to($opt_r);
  $smtp->data();
  $smtp->datasend("MIME-Version: 1.0\n");
  $smtp->datasend("Content-Type: text/html; charset=iso-8859-1\n");
  $smtp->datasend("To: $opt_r\n");
  $smtp->datasend("From: $opt_f\n");
  $smtp->datasend("Subject: $opt_s\n");
  foreach $line (@body_data)
    {
      $smtp->datasend("$line");
    }
  $smtp->dataend();
  $smtp->quit;

sub error {
  my $msg = shift;
  print STDERR "html-mailer.pl: $msg\n";
  exit 1;
}

sub print_usage {
  print STDERR <<EOF

  html-mailer.pl - HTML Formatted Message Mailer

  Usage: html-mailer.pl -f FROM -r RECIP -s SUBJ -m SMTP_HOST -b HTML_FILE

  Sends an email to the specified address, filling the message body with the
  HTML formatted file specified.

EOF
}

SCRIPTCREATOR

chmod 0700 ./html-mailer.pl

###############################################################################


Enable outbound SMTP
Now that we've got a script that we can send test messages with, we need to enable outbound SMTP through the ESX firewall on the ESX server that will have the script scheduled from a cron job. Just type this command as root to open the port:

  # Execute as root
  esxcfg-firewall --openPort 25,tcp,out,SMTP


If your Exchange or SMTP server is reachable from the Service Console network, execute html-mailer.pl with the appropriate parameters, and specify any old text file:

  ./html-mailer.pl -f me@mydomain.dom \
                   -r me@mydomain.dom \
                   -s "Test message" \
                   -m exchange.mydomain.dom \
                   -b ./testfile.txt


The Service Console can't reach the Exchange server...
No worries, as long as you're able to reach the VirtualCenter server, we can install the SMTP service and set it up to forward to the Exchange server. To install SMTP on a Windows 2003 server, do the following:

  • Open the Control Panel > Add or Remove Programs > Add/Remove Windows Components > double click Application Server > and then double click Internet Information Server (IIS). Put a check next to SMTP Service and click OK, OK, and Next

  • After the SMTP install is complete, open the Start Menu > Programs > Administrative Tools > Internet Information Services (IIS) Manager, then right click Default SMTP Virtual Server and select Properties

  • In the General tab, drop down the IP address: to the IP address in the Service Console network, if different from the LAN. This will prevent the SMTP service from popping up on your network security guy's port scans :)

  • In the Access tab, click the Connection button, choose the Only the list below radio button, then click Add to add the appropriate subnet address and mask to the Group of computers option, or add each ESX server one at a time

  • In the Access tab again, click the Relay button, choose the Only the list below radio button, then click Add to add the appropriate subnet address and mask to the Group of computers option, or add each ESX server one at a time. Uncheck the option Allow all computers which successfully authenticate to relay

  • On the Delivery tab, click the Advanced button and add your Exchange server information in the Smart host: box. By specifying a smart host, the SMTP server will simply forward everything to the Exchange server, letting it make all the decisions about which domains to accept mail for, etc.

  • Now test out the SMTP forwarder by using telnet to initiate a SMTP session from the ESX server that will be sending the messages:
    
     telnet virtualcenter.lab.local 25
     ehlo
     mail from:spongebob@lab.local
     rcpt to:administrator@lab.local
     data
     Subject:test
     .
     quit
    

One script to rule them all
Almost there, so let's recap what we've done so far. In Part 1, we created the health check script that will run on each ESX server and send key performance stats and scaled histograms to the terminal. Then in Part 2, we covered how to distribute public keys so the script can be executed on several ESX servers via SSH. So far in Part 3, we've looked at a perl script that will email the combined script output, and now we need to create a script to tie it all together, and then schedule the script from a cron job.

Let's break down the main components of the script. First of all, if ssh-agent isn't running, the script isn't going to get very far, so we'll use pgrep to check for the process and exit if it's not found:

  if ! pgrep ssh-agent >/dev/null; then
    echo "The ssh-agent process does not appear to be running, exiting"
    exit 1
  fi

We need to source .ssh-agent, the file with the ssh-agent PID and socket info set up by the start-ssh-agent.sh script, or exit if it doesn't exist:

  source "${HOME}/.ssh-agent" >/dev/null || exit 1

Since we'll be running everything from an ESX Service Console, and that server is likely to be part of the health check, we should compare the list of ESX hosts to the local hostname so we don't open a SSH connection to the local machine. We use cut here to strip off the domain name so we'll match whether the FQDN or just the bare hostname is specified:

  THISHOST=$(hostname | cut -d . -f 1)

  for host in $@; do
    if [ $(echo $host | cut -d . -f 1) = $THISHOST ]; then
      "${RUNDIR}/esx-report.sh" >> "$TEMPTEXT"
    else
      ssh -q $host "$(cat "${RUNDIR}/esx-report.sh")" >> "$TEMPTEXT" || \
        printf "WARNING: SSH connection to $host failed\n\n\n\n" >> "$TEMPTEXT"
    fi
  done

After the health check script has looped through the list of ESX hosts, we'll start building the HTML file with the necessary tags. Setting the font size for the pre tag is the secret sauce for getting the email to display perfectly on a BlackBerry:

  cat > "$TEMPHTML" <<-'HEADEREOF'
	<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
	<html>
	<head>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
	<style type="text/css">
	body { font-family: monospace; font-size: 12px }
	pre { font-family: monospace; font-size: 12px }
	</style>
	</head>
	<body>
	<pre>
	HEADEREOF

If you need to use < or > symbols in a HTML document, you have to specify the actual ASCII code of the character, as HTML considers words wrapped in those symbols to be tags. We'll use a sed filter to replace all the >'s with the ASCII equivalent, and add a color tag to any lines with the word WARNING to make it stand out:

  cat "$TEMPTEXT" | \
  sed -e 's/>/\&#62/g' \
      -e 's/WARNING:.*/<span style="color: red">&<\/span>/' >> "$TEMPHTML"
Then we'll add the closing tags for everything to the end of the HTML file:

  cat >> "$TEMPHTML" <<-'FOOTEREOF'
	</pre>
	</body>
	</html>
	FOOTEREOF

And finally, we'll execute html-mailer.pl with the appropriate parameters. You'll need to change this section of the script for your environment:

  "${RUNDIR}/html-mailer.pl" -f esx-report@lab.local \
                             -r administrator@lab.local \
                             -s "ESX Health Report" \
                             -m lab-vc \
                             -b "$TEMPHTML"

Here's the run-esx-report.sh script. Remember to change the email address and mail server parameters for your environment:

###############################################################################
#
#  run-esx-report.sh
#
###############################################################################
#
#  To create the run-esx-report.sh script in the ~/esx-report directory, copy
#  this entire code segment into your shell.
#  If you'd rather copy just the script, select everything between the
#  SCRIPTCREATOR limit strings.
#
#  putty will ignore all the tabs, making the copied script quite ugly
#
###############################################################################

# If the ~/esx-report directory exists, cd to it so the script is created there
[ -d ~/esx-report ] && cd ~/esx-report

cat > ./run-esx-report.sh <<'SCRIPTCREATOR'
#! /bin/bash
  PATH="/bin:/usr/bin"

  if [ -z $1 ]; then
    echo "No ESX hosts specified, exiting"
    exit 1
  fi

  if ! pgrep ssh-agent >/dev/null; then
    echo "The ssh-agent process does not appear to be running, exiting"
    exit 1
  fi

  RUNDIR=$(dirname "$(which "$0")")

  source "${HOME}/.ssh-agent" >/dev/null || exit 1

  THISHOST=$(hostname | cut -d . -f 1)

  TEMPTEXT=$(mktemp "${RUNDIR}/temptext.XXXXXXXXXX")

  TEMPHTML=$(mktemp "${RUNDIR}/temphtml.XXXXXXXXXX")

  for host in $@; do
    if [ $(echo $host | cut -d . -f 1) = $THISHOST ]; then
      "${RUNDIR}/esx-report.sh" >> "$TEMPTEXT"
    else
      ssh -q $host "$(cat "${RUNDIR}/esx-report.sh")" >> "$TEMPTEXT" || \
        printf "WARNING: SSH connection to $host failed\n\n\n\n" >> "$TEMPTEXT"
    fi
  done

  cat > "$TEMPHTML" <<-'HEADEREOF'
	<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
	<html>
	<head>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
	<style type="text/css">
	body { font-family: monospace; font-size: 12px }
	pre { font-family: monospace; font-size: 12px }
	</style>
	</head>
	<body>
	<pre>
	HEADEREOF

  cat "$TEMPTEXT" | \
  sed -e 's/>/\&#62;/g' \
      -e 's/WARNING:.*/<span style="color: red">&<\/span>/' >> "$TEMPHTML"

  cat >> "$TEMPHTML" <<-'FOOTEREOF'
	</pre>
	</body>
	</html>
	FOOTEREOF

  "${RUNDIR}/html-mailer.pl" -f esx-report@yourdomain.dom \
                             -r administrator@yourdomain.dom \
                             -s "ESX Health Report" \
                             -m exchange.yourdomain.com \
                             -b "$TEMPHTML"

  rm -f "$TEMPTEXT"; rm -f "$TEMPHTML"

SCRIPTCREATOR

chmod 0700 ./run-esx-report.sh

###############################################################################


To cron foo, thanks for everything
Still with us? One more step, and it's an easy one. We'll add a cron job to run the script at 7:10 AM every morning. Remember to add the job for the user account you distributed SSH keys for.

To edit the cron entries for the user, type:

  crontab -e

This starts vi and opens up the user's crontab. To enter insert mode, type i

Assuming you've set everything up using the code segments used in this series, to add an entry for 7:10 AM, type this line, replacing ESX LIST with a space separated list of ESX hosts:

  10 7 * * * ${HOME}/esx-report/run-esx-report.sh ESX LIST >/dev/null 2>&1

After adding the entry, press the Esc key, and type :wq to write the crontab and quit.

If you have a long list of hosts, put them all in a text file, separated by spaces or each on its own line, and use command substitution to feed the list to run-esx-report.sh

  run-esx-report.sh $(cat ${HOME}/esx-report/hostlist.txt)

There's more?!
What if we wanted to trigger an email warning if an ESX host exceeds a threshold value? As we'll see in Part 4, we can do this easily with a quick modification to the run-esx-report.sh script.

2 comments:

  1. What modifications should be made to the script if not using the ssh-agent functionality? That is, it would be run from a single host.

    Thanks.

    ReplyDelete
  2. It's pretty easy to modify it for one host. First of all, just ignore everything from Part 2, since you won't need any of the SSH functionality.


    Then from the run-esx-report.sh script in Part 3, remove this check for a list of hosts to query, as we'll always be running on the local host:


    if [ -z $1 ]; then
    echo "No ESX hosts specified, exiting"
    exit 1
    fi



    And remove this check to see if ssh-agent is running:


    if ! pgrep ssh-agent >/dev/null; then
    echo "The ssh-agent process does not appear to be running, exiting"
    exit 1
    fi



    And remove the source command on the .ssh-agent file, since it won't be there:


    source "${HOME}/.ssh-agent" >/dev/null || exit 1


    We don't need to capture the local hostname, as we're always going to run anyway, so remove this:


    THISHOST=$(hostname | cut -d . -f 1)



    You could leave this loop, but it's probably better to replace this whole section:


    for host in $@; do
    if [ $(echo $host | cut -d . -f 1) = $THISHOST ]; then
    "${RUNDIR}/esx-report.sh" >> "$TEMPTEXT"
    else
    ssh -q $host "$(cat "${RUNDIR}/esx-report.sh")" >> "$TEMPTEXT" || \
    printf "WARNING: SSH connection to $host failed\n\n\n\n" >> "$TEMPTEXT"
    fi
    done


    ...with this command to just run the script on the local host:


    "${RUNDIR}/esx-report.sh" >> "$TEMPTEXT"



    And when you schedule the cron job, you won't need to specify a list of hosts, just execute run-esx-report.sh:


    10 7 * * * ${HOME}/esx-report/run-esx-report.sh >/dev/null 2>&1



    If you make these changes, let me know if it works or not. Thanks!

    ReplyDelete