4
\$\begingroup\$

I have a well functioning Nagios Core system set up. I wanted to make our alerts both easier to read pertinent information quickly as well as provide additional details from custom host and service variables. While I have touched on Perl in the past this is my first real Perl script. The larger portion of CSS and HTML structure com from fm4dd.com. The email logic was taken from the Mail::Sendmail documentation FAQ.

#!/usr/bin/perl
use strict;
use warnings;
use Getopt::Long;
use Mail::Sendmail;
use MIME::QuotedPrint;
use HTML::Entities;

my $notificationType = '';
my $hostAlias = '';
my $hostAddress = '';
my $serviceDescription = '';
my $serviceState = '';
my $eventDateTime = '';
my $serviceOutput = '';
my $verbose = 0;
my $companyName = 'My Company';
my %ancillaryValues = ();

##############################
# Handle command line options
##############################

GetOptions(
    'notificationType=s'    =>  \$notificationType,
    'hostAlias=s'			=>	\$hostAlias,
    'hostAddress=s'         =>  \$hostAddress,
    'serviceDescription=s'	=>	\$serviceDescription,
    'serviceState=s'        =>  \$serviceState,
    'eventDateTime=s'		=>	\$eventDateTime,
    'serviceOutput=s'       =>  \$serviceOutput,
    'optional=s'			=>	\%ancillaryValues,
    'verbose'				=>	\$verbose
);

##############################
# CSS for mail
##############################

(my $style = qq(
    !<style type="text/css">
    !    body {text-align: center; font-family: Verdana, sans-serif; font-size: 10pt;}
    !    img.logo {float: left; margin: 10px 10px 10px; vertical-align: middle}
    !    img.link {float: right;  margin: 0px 1px; vertical-align: middle}
    !    table.fixed {width: 600px; text-align:center; margin-left: auto; margin-right: auto; border: 1px solid black; table-layout:fixed;}
    !    th {white-space: nowrap; width: 180px;}
    !    table.fixed td {word-wrap:break-word;}
    !    th.even {background-color: #D9D9D9;}
    !    td.even {background-color: #F2F2F2;}
    !    th.odd {background-color: #F2F2F2;}
    !    td.odd {background-color: #FFFFFF;}
    !    th,td {font-family: Verdana, sans-serif; font-size: 10pt; text-align:left;}
    !    th.customer {width: 600px; background-color: #004488; color: #ffffff;}
    !    td.problem {background-color: #FF8080;}
    !    td.recovery {background-color: #80FF80;}
    !    td.acknowledgement {background-color: #FFFF80;}
    !    td.downtimestart {background-color: #80FFFF;}
    !    td.downtimeend {background-color: #80FF80;}
    !    td.downtimecancelled {background-color: #FFFF80;}
    !    td.flappingstart {background-color: #FF8080;}
    !    td.flappingstop {background-color: #80FF80;}
    !    td.flappingdisabled {background-color: #FFFF80;}
    !    td.test {background-color: #80FFFF;}
    !    td.critical {background-color: #FFAA60;}
    !    td.warning {background-color: #FFFF80;}
    !    td.ok {background-color: #80FF80;}
    !    td.unknown {background-color: #80FFFF;}
    !    th.header{text-align: center; font-family: Verdana, sans-serif; font-size: 12pt;}
    !    p.foot {width: 602px; background-color: #004488; color: #ffffff; margin-left: auto; margin-right: auto;}
    !</style>
)) =~ s/^[^\S\n]+[!\n]?//mg;

##############################
# Function and Subroutines 
##############################

sub buildHTMLContent {
    # Function will build the html content to be used in the email.
    # First create a table with all of the common service information.
    # A second optional table will be created if supplemental information is provided.
    # CSS and other wrapper HTML tags are added after. 
    my $ancillaryHTMLTable = '';

    # Build the main table with service information
    (my $serviceHTMLTable = qq(
    !<table class=\"fixed\">
    !    <tr><th colspan=\"2\" class=\"header\">Nagios Monitoring System Notification</th></tr>
    !    <tr><th colspan=\"2\" class=\"customer\">$companyName</th></tr>  
    !    <tr><th class=\"even\">Notification Type:</th><td class=\"$notificationType\">$notificationType</td></tr>
    !    <tr><th class=\"odd\">Host:</th><td class=\"odd\">$hostAlias</td></tr>
    !    <tr><th class=\"even\">Check:</th><td class=\"even\">$serviceDescription</td></tr>
    !    <tr><th class=\"odd\">State:</th><td class=\"$serviceState\">$serviceState</td></tr>
    !    <tr><th class=\"even\">Address:</th><td class=\"even\">$hostAddress</td></tr>
    !    <tr><th class=\"odd\">Date/Time:</th><td class=\"odd">$eventDateTime</td></tr>
    !    <tr><th class=\"even\">Additional Info:</th><td class=\"even\">$serviceOutput</td></tr>
        !</table>
    ))=~ s/^[^\S\n]+[!\n]?//mg; 

    # Build the optional second table from %ancillaryValues 
    # Check if there are any values to be processed. 
    if((keys %ancillaryValues) > 0){
        my $rows = '';
        my $parity = 'even';     # This will get toggled as we build the rows. Used for colour formatting.
        # Build each row. If the data appears to be a hyperlink, format it as such.
        foreach(keys %ancillaryValues) {
            # Check if the data starts with a link.
            if($ancillaryValues{$_} =~ /^https?://///){
            # This is a link.
        $rows .= "<tr><th class=\"$parity\">$_:</th><td class=\"$parity\"><a href=\"$ancillaryValues{$_}\">$ancillaryValues{$_}</a></td></tr>"
            } else {
                # This is not a link.
        $rows .= "<tr><th class=\"$parity\">$_:</th><td class=\"$parity\">$ancillaryValues{$_}</td></tr>"
            }

            # Switch parity
            $parity = $parity eq 'even' ? 'odd' : :'even';                  
    }

    # Insert the rows into the table
    ($ancillaryHTMLTable = qq(
        !<table class="fixed">
        !    <tr><th colspan=\"2\" class=\"header\">Ancillary Infomation</th></tr>
        !    $rows
        !</table>
        !))=~ s/^[^\S\n]+[!\n]?//mg; 
    }

    (my $html = qq(
        !<html>\n<head>\n$style\n<head>\n<body>\n
        !$serviceHTMLTable
        !<br>
        !$ancillaryHTMLTable
        !</body>\n</html>
    )) =~ s/^[^\S\n]+[!\n]?//mg; 
    return $html;
}

sub sendHTMLMail {
    # This function will send an html formatted mail
    # of the service data

    my $boundary = "====" . time() . "====";

    (my $messageText = qq(
    !***** Nagios *****
    !
    !Notification Type: $notificationType
    !
    !Service: $serviceDescription
    !Host: $hostAlias
    !Address: $hostAddress
    !State: $serviceState
    !
    !Date/Time: $eventDateTime
    !
    !Additional Info: 
    !
    !${\uc $serviceState}: $serviceOutput
    !)) =~ s/^[^\S\n]+[!\n]?//mg; 


    my $plain = encode_qp $messageText;

    my %mail = (
    To              => 'myemail@address.com',
    From            => 'script@company.com',
    Subject         => "Testing HTML Message",
    'Content-Type'  => "multipart/alternative; boundary=\"$boundary\""
    );

    $boundary = '--'.$boundary; 

    ($mail{body} = qq(
        !$boundary
        !Content-Type: text/plain; charset="iso-8859-1"
        !Content-Transfer-Encoding: quoted-printable
        !
        !$plain
        !
        !$boundary
        !Content-Type: text/html; charset="iso-8859-1"
        !Content-Transfer-Encoding: quoted-printable
        !
        !${\encode_qp(buildHTMLContent)}
        !$boundary--
        )) =~ s/^[^\S\n]+[!\n]?//mg; 

    sendmail(%mail) or die $Mail::Sendmail::error;

    print "Mail was sent succesfully: \n", $Mail::Sendmail::log;
    print "\n";
};


##############################
# Print optional verbose information
##############################
if($verbose == 1){
    print "Time time is:" . time() . "\n";
    print "The service is: '$serviceDescription'\n";

    # Check to see if we have any extra values passed
    if((keys %ancillaryValues) > 0){
        foreach(keys %ancillaryValues) {
        print "Optional Value: $_ / $ancillaryValues{$_}\n";
    }
    }
}

sendHTMLMail;

I tried to minimize reliance on modules so while suggesting some that would have reduced the code it good to know I am hoping not to add any more.

Sample call:

perl nagios_send_html_service_mail.pl --serviceDescription "CPU Usage" --hostAlias yrp_7f_server 
    --hostAddress 10.10.13.33 --notificationType PROBLEM --eventDateTime `date '+%d/%m/%Y'` 
    --serviceOutput "5m: average load 100%  critical, 1m: average load 100%  critical, 30s: average load 100%  critical" 
    --serviceState Critical --opt This=That --verbose --opt 'Document Manager'='http://kanimage02:81/asset.aspx?AssetID=6456' --option CodeReview=Awesome

In a production environment you would build a Nagios command string using macros that would populate those parameters with real information on execution. I have used dummy data for this example.

Example email generated.

Email sample created

HTML of the above table

<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"><style type="text/css">
body {text-align: center; font-family: Verdana, sans-serif; font-size: 10pt;}
img.logo {float: left; margin: 10px 10px 10px; vertical-align: middle}
img.link {float: right;  margin: 0px 1px; vertical-align: middle}
table.fixed {width: 600px; text-align:center; margin-left: auto; margin-right: auto; border: 1px solid black; table-layout:fixed;}
th {white-space: nowrap; width: 180px;}
table.fixed td {word-wrap:break-word;}
th.even {background-color: #D9D9D9;}
td.even {background-color: #F2F2F2;}
th.odd {background-color: #F2F2F2;}
td.odd {background-color: #FFFFFF;}
th,td {font-family: Verdana, sans-serif; font-size: 10pt; text-align:left;}
th.customer {width: 600px; background-color: #004488; color: #ffffff;}
td.problem {background-color: #FF8080;}
td.recovery {background-color: #80FF80;}
td.acknowledgement {background-color: #FFFF80;}
td.downtimestart {background-color: #80FFFF;}
td.downtimeend {background-color: #80FF80;}
td.downtimecancelled {background-color: #FFFF80;}
td.flappingstart {background-color: #FF8080;}
td.flappingstop {background-color: #80FF80;}
td.flappingdisabled {background-color: #FFFF80;}
td.test {background-color: #80FFFF;}
td.critical {background-color: #FFAA60;}
td.warning {background-color: #FFFF80;}
td.ok {background-color: #80FF80;}
td.unknown {background-color: #80FFFF;}
th.header{text-align: center; font-family: Verdana, sans-serif; font-size: 12pt;}
p.foot {width: 602px; background-color: #004488; color: #ffffff; margin-left: auto; margin-right: auto;}
</style>

<head>
<body>


<table class="fixed">
<tr><th colspan="2" class="header">Nagios Monitoring System Notification</th></tr>
<tr><th colspan="2" class="customer">My Company</th></tr>  
<tr><th class="even">Notification Type:</th><td class="PROBLEM">PROBLEM</td></tr>
<tr><th class="odd">Host:</th><td class="odd">yrp_7f_server</td></tr>
<tr><th class="even">Check:</th><td class="even">CPU Usage</td></tr>
<tr><th class="odd">State:</th><td class="Critical">Critical</td></tr>
<tr><th class="even">Address:</th><td class="even">10.10.13.33</td></tr>
<tr><th class="odd">Date/Time:</th><td class="odd">01/12/2016</td></tr>
<tr><th class="even">Additional Info:</th><td class="even">5m: average load 100%  critical, 1m: average load 100%  critical, 30s: average load 100%  critical</td></tr>
</table>

<br>

<table class="fixed">
<tr><th colspan="2" class="header">Ancillary Infomation</th></tr>
<tr><th class="even">CodeReview:</th><td class="even">Awesome</td></tr><tr><th class="odd">Document Manager:</th><td class="odd"><a href="http://kanimage02:81/asset.aspx?AssetID=6456">http://kanimage02:81/asset.aspx?AssetID=6456</a></td></tr><tr><th class="even">This:</th><td class="even">That</td></tr>
</table>

</body>
</html>

My Perl build details

This is perl 5, version 16, subversion 3 (v5.16.3) built for x86_64-linux-thread-multi

I intend to make a host email script from this as well. It will be mostly the same except the main table will be displaying different variables.

\$\endgroup\$
6
  • \$\begingroup\$ Why do you not want to add more modules? This decision is not making your life easier. Can you explain it reasonably? What advantages does it give you? \$\endgroup\$
    – simbabque
    Dec 2, 2016 at 15:04
  • \$\begingroup\$ @simbabque Partially because I want to learn the hard way for some things. Also I worry about adding stuff I don't really need. These will get called rather frequently so I wonder about performance issues. I have no specific knowledge of modules and memory management so that assertion is liking flawed. Hoping for responses like "It would be easier if you used [module] but here is how I would change what you are doing. \$\endgroup\$
    – Matt
    Dec 2, 2016 at 15:07
  • \$\begingroup\$ I see. What do you mean by frequently? Several times a second? \$\endgroup\$
    – simbabque
    Dec 2, 2016 at 15:10
  • \$\begingroup\$ Not likely but the potential is there. At least once a minute is more realistic. There is more that happens for an "event" so I wanted to try and keep it learn. That goes both ways so potentially using a module is more efficient. For instance I use Getop::long only because that is so much easier then doing it myself. The rest did not seem difficult to me aside from the html and sendmail modules which I would find harder to deal without. It's my first day \$\endgroup\$
    – Matt
    Dec 2, 2016 at 15:15
  • \$\begingroup\$ How to configure the code in /usr/local/nagios/etc/objects/commands.cfg to take this effect? Generally, i am asking where to save the code and how can we define the command to get the output? Can you please explain? \$\endgroup\$
    – user156014
    Dec 17, 2017 at 17:37

1 Answer 1

2
\$\begingroup\$

This is mostly about style.

  • Indent and align your code properly. This will save you a lot of time later when you have to go back to change stuff.

    my $notificationType   = '';
    my $hostAlias          = '';
    my $hostAddress        = '';
    my $serviceDescription = '';
    my $serviceState       = '';
    my $eventDateTime      = '';
    my $serviceOutput      = '';
    my $verbose            = 0;
    my $companyName        = 'My Company';
    my %ancillaryValues    = ();
    

    This is much easier to read as the zig-zag you have there. The same goes for the GetOptions call (though I think that's just the real tabs confusing SO). If you don't want to do this yourself, take a look at perltidy. It's an auto-indentation tool that does this for you. It's highly configurable and integrates nicely into almost all editors or IDEs.

  • You don't need to escape double quotes "" in strings that you built with qq(). It's horrible to read with all the backslashes. Get rid of them.

    (my $serviceHTMLTable = qq(
        !<table class="fixed">
        !    <tr><th colspan="2" class="header">Nagios Monitoring System Notification</th></tr>
    
  • You can use say instead of print "\n". It's a built-in that needs to be enabled by use feature 'say'; at the top of your program, with the other pragmas.

    say "Time time is: ", time;
    

    Note that you don't need the parenthesis on the time call. Also, print and say take an argument list. You can concatenate all string-parts into one argument with ., but you don't have to. I prefer the comma , notation.

  • The canonical way to name variables and subs in Perl is all lower case, snake case.

    my $notification_type   = '';
    my $host_alias          = '';
    
    sub build_html_content { ... }
    

    I know GetOptions doesn't do that, but you still should. It's also easier to read.

There really isn't anything wrong with your program. You have verbose names for stuff, you have good comments that not only say what is going on, but often also explain why. Well done.

If you did want to use different modules, I would take a look at a templating system. There are lots of lightweight ones, but it's not really needed. This is so short that it's fine.

For your email, you could go with considerably less code by using Email::Stuffer. It does everything for you with basically the same underlying stuff, just way easier to use.

If you don't intend to change the CSS, you could get rid of that post-processing of the block of CSS, and instead include it already minified. Add a comment on how to create that, and add the raw source plus the code you used to minify in the __END__ section as documentation. That will make the pure code easier to read and save some (negigible) time during execution. That could look like this:

my $style = q(body{text-align:center;fo...); # minified, see below

# ...

__END__
# Use this program to create the minified version. 
# Copy into a new script, alter and run, then replace the minified stuff above.
use strict;
use warnings;
use Foo::CSS::Minifier; # I made that up

Foo::CSS::Minifier->new->minify(<<CSS);
body {text-align: center; fo...
# more css here
CSS
\$\endgroup\$
4
  • \$\begingroup\$ The indent issue are mostly from SO markup. Looking at the code in VI it looks better. Comments about that still work though. When I tried to fix it I made it worse. \$\endgroup\$
    – Matt
    Dec 2, 2016 at 17:21
  • \$\begingroup\$ I see. Yeah when I copied it the tabs were there and it also looked fine in my Komodo IDE. Just ignore it, the rest of the post is sound. :-) \$\endgroup\$
    – simbabque
    Dec 2, 2016 at 17:23
  • \$\begingroup\$ Do you have an example you can link me to for your minify concept in your last paragraph? The CSS is in the beginning so I don't have to go hunting for it for minor colour changes and such. Near the top so it would be easy to locate.... in theory. \$\endgroup\$
    – Matt
    Dec 2, 2016 at 17:26
  • \$\begingroup\$ @Matt I see what you mean. The question is, would you actually do that? If you do, it makes sense to keep it like it is. If you don't, I'd compile it in a way. There should be CSS minifiers on CPAN. I have not used any of them, so I don't know which one is good. I edited the answer with a non-working example of what I'm talking about. \$\endgroup\$
    – simbabque
    Dec 2, 2016 at 17:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.