#!/usr/bin/perl -w

# esxi_health_status.pl - generate health report for ESXi server, optionally e-mail it somewhere
#
#   Copyright (C) 2011-2017  Jernej Simončič
#
#   This program is free software; you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation; either version 2 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program; if not, write to the Free Software
#   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301, USA
#
#Changelog:
#
#2017-03-08: 1.0.3
# - add SSL/TLS support
#
#2011-10-01: 1.0.2
# - fixed month in status
#
#2011-09-07: 1.0.1
# - add SMTP authentication support
#
#2011-09-06: 1.0
# - first public release

use strict;
use warnings;
use VMware::VIRuntime;
use VMware::VILib;
use Net::SMTP;
use utf8;

my %opts = (
	'mailserver' => {
		type => '=s',
		help => 'SMTP server address',
		required => 0,
	},
	'sender' => {
		type => '=s',
		help => 'sender address',
		required => 0,
	},
	'recipient' => {
		type => '=s',
		help => 'recipient addresses (separate multiple addresses with ;)',
		required => 0,
	},
	'mailuser' => {
		type => '=s',
		help => 'username for SMTP authentication',
		required => 0,
	},
	'mailpass' => {
		type => '=s',
		help => 'password for SMTP authentication',
		required => 0,
	},
	'mailssl' => {
		type => '',
		help => 'use SSL/TLS when connecting to mail server',
		required => 0,
	},
	'mailhtml' => {
		type => '',
		help => 'send report e-mail in HTML format',
		required => 0,
	},
	'maillevel' => {
		type => '=i',
		help => 'send e-mail only if warning level is achieved (0 = all, 1 = unknown, 2 = warnings, 3 = errors)',
		required => 0,
		default => 0,
	},
	'ignore' => {
		type => '=s',
		help => 'ignore items matching regexp',
		required => 0,
	},
	'textreport' => {
		type => '=s',
		help => 'text report file',
		required => 0,
	},
	'htmlreport' => {
		type => '=s',
		help => 'html report file',
		required => 0,
	},
);

my $ignore;
my %hwstate = (
	'green' => 0,
	'none' => 0,
	'ignored' => 0,
	'unknown' => 1,
	'yellow' => 2,
	'red' => 3,
);
my @hwstate = ('green','unknown','yellow','red');
my %hwmap = (
	'cpu' => 'CPU',
	'memory' => 'Memory',
	'storage' => 'Storage',
	'fan' => 'Fan',
	'power' => 'Power',
	'system' => 'System',
	'temperature' => 'Temperature',
);

sub get_time()
{
	my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
	
	return sprintf('%04d-%02d-%02d %02d:%02d:%02d',$year+1900,$mon+1,$mday,$hour,$min,$sec);
}

sub htmlize($)
{
	my ($text) = @_;
	$text =~ s,&,&amp;,g;
	$text =~ s,<,&lt;,g;
	return $text;
}

sub trim($) #remove spaces at the start of text
{
	my ($text) = @_;
	$text =~ s:^\s+::;
	return $text;
}

sub main()
{
	my %status;
	
	$ignore = Opts::get_option('ignore');

	my $ev_host = Vim::find_entity_view(view_type => 'HostSystem');

	$status{name} = $ev_host->name;
	$status{fullname} = $ev_host->summary->config->product->fullName;
	$status{time} = get_time();

	get_health($ev_host,\%status);

	write_status(Opts::get_option('textreport'), status_text(\%status), 'Text');
	write_status(Opts::get_option('htmlreport'), status_html(\%status), 'HTML');
	send_status(\%status);
}

sub get_hwelement($$\%)
{
	my ($type,$hwelement,$status) = @_;

	return unless $hwelement;

	foreach my $elem (@$hwelement) {
		my $state = lc($elem->status->key);

		$state = 'ignored' if ($ignore && $elem->name =~ /$ignore/);

		push @{$status->{sensors}->{$type}}, {
			state => $state,
			name => trim($elem->name),
		};

		if ($type eq 'storage' && $elem->operationalInfo) { #storage has some additional info, include it if present
			my %info;
			foreach (@{$elem->operationalInfo}) {
				$info{trim($_->property)} = trim($_->value);
			}
			if (%info) {
				$status->{sensors}->{$type}->[$#{$status->{sensors}->{$type}}]->{operational} =
					{%info};
			}
		}

		$status->{sensorstates}->{$type}->{$state}++;

		$status->{states}->{$state}++;
	}
}

sub get_numsensor($\%)
{
	my ($numsensor,$status) = @_;

	return unless $numsensor;

	foreach my $elem (@$numsensor) {
		my $state = 'none';
		$state = lc($elem->healthState->key) if ($elem->healthState);

		$state = 'ignored' if ($ignore && $elem->name =~ /$ignore/);

		my $units = $elem->baseUnits;
		$units .= '/' . $elem->rateUnits if ($elem->rateUnits);

		push @{$status->{sensors}->{$elem->sensorType}}, {
			state => $state,
			name => trim($elem->name),
			reading => $elem->currentReading * 10 ** $elem->unitModifier,
			units => $units,
		};

		$status->{sensorstates}->{$elem->sensorType}->{$state}++;

		$status->{states}->{$state}++;
	}
}

sub get_health($\%)
{
	my ($ev_host,$status) = @_;

	my $healthstat = Vim::get_view(mo_ref => $ev_host->configManager->healthStatusSystem)->runtime;

	if (my $hwstatus = $healthstat->hardwareStatusInfo) {
		get_hwelement('cpu', $hwstatus->cpuStatusInfo, %$status);
		get_hwelement('memory', $hwstatus->memoryStatusInfo, %$status);
		get_hwelement('storage', $hwstatus->storageStatusInfo, %$status);
	}
	if (my $sysstatus = $healthstat->systemHealthInfo) {
		get_numsensor($sysstatus->numericSensorInfo, %$status);
	}

	foreach my $sensor (keys %{$status->{sensorstates}}) {
		foreach my $state (keys %{$status->{sensorstates}->{$sensor}}) { #store the worst state in each group for sorting
			if (!defined($status->{worststate}->{$sensor}) || $hwstate{$state} > $status->{worststate}->{$sensor}) {
				$status->{worststate}->{$sensor} = $hwstate{$state};
			}
			if (!defined($status->{worst}) || $hwstate{$state} > $status->{worst}) { #overall worst state
				$status->{worst} = $hwstate{$state};
			}
		}
	}
}

#sort by warning level first, then alphabetically (case-insensitive)
sub status_sort($$)
{
	if ($_[0]->{state} ne $_[1]->{state}) {
		return $hwstate{$_[1]->{state}} <=> $hwstate{$_[0]->{state}};
	} else {
		return lc($_[0]->{name}) cmp lc($_[1]->{name});
	}
}

#same as above, just data in different format
sub group_sort($$)
{
	if ($_[0]->[2] != $_[1]->[2]) {
		return $_[1]->[2] <=> $_[0]->[2];
	} else {
		return lc($_[0]->[1]) cmp lc($_[1]->[1]);
	}
}

sub text_state($$\%)
{
	my ($oktext,$start,$states) = @_;

	my $out = '';
	if ($states->{'red'}) {
		$out .= $start . $states->{'red'}.' errors';
	}
	if ($states->{'yellow'}) {
		$out .= ( $out eq '' ? $start : ', ' ).$states->{'yellow'}.' warnings';
	}
	if ($states->{'unknown'}) {
		$out .= ( $out eq '' ? $start : ', ' ).$states->{'unknown'}.' unknown readings';
	}

	$out = $oktext if $out eq '';

	return $out;
}

sub status_text(\%)
{
	my ($status) = @_;

	my %statemap = (
		'green' => '-',
		'none' => '-',
		'ignored' => '-',
		'unknown' => '?',
		'yellow' => '*',
		'red' => '!',
	);

	my $out = 'Health report for '.$status->{name}."\n\n";
	$out .= $status->{fullname}."\n" if $status->{fullname};
	$out .= 'Time: '.$status->{time}."\n\n";
	$out .= text_state('No problems detected','',%{$status->{states}})."\n\n";

	my @groups;

	for my $type (keys %{$status->{sensors}}) {
		my $group = ( $hwmap{$type} ? $hwmap{$type} : $type );
		$group .= text_state('',': ',%{$status->{sensorstates}->{$type}});
		$group .= "\n";

		foreach my $sensor (sort status_sort @{$status->{sensors}->{$type}}) {
			$group .= '  ' . $statemap{$sensor->{state}} . ' ' . $sensor->{name};

			if ($sensor->{reading} && $sensor->{units} ne '') {  #also conveniently skips all sensors that just display 0 without an unit
				$group .= ': ' . $sensor->{reading} . ' ' . $sensor->{units};
			}
			$group .= "\n";

			if ($sensor->{operational}) {
				foreach (sort keys %{$sensor->{operational}}) {
					$group .= "      " . $_;
					$group .= ': ' . $sensor->{operational}->{$_} if $sensor->{operational}->{$_};
					$group .= "\n";
				}
			}

		}
		$group .= "\n";

		push @groups, [$group, $type, $status->{worststate}->{$type}];
	}

	foreach my $group (sort group_sort @groups) { #sort items with warnings on top
		$out .= $group->[0];
	}

	$out .= "Key: - normal, ? unknown, * warning, ! error\n";

	return $out;
}

sub html_head($$)
{
	my ($name, $title) = @_;

	return <<_END
<html>
<head><title>Health report for $name$title</title>
<style>
* {
	font-family: "Verdana", sans-serif;
}
html {
	font-family: "Verdana", sans-serif;
	font-size: 9pt;
}
h1 {
	font-size: 14pt;
	font-weight: normal;
}
h2 {
	font-size: 12pt;
	margin-top: 1em;
	margin-bottom: 0.3em;
}
h2 span {
	font-weight: normal;
}
ul {
	margin-top: 0;
	margin-bottom: 0;
}
.unknown {
	font-style: italic;
}
.yellow {
	color: #f90;
}
.red {
	color: #f00;
}
</style>
</head><body>
<h1>Health report for <b>$name</b></h1>
_END
}

sub html_tail()
{
	return <<_END
<p>Key: <span class='green'>normal</span>, <span class='unknown'>unknown</span>,
<span class='yellow'>warning</span>, <span class='red'>error</span></p>
</body>
</html>
_END
}

sub html_state($$\%)
{
	my ($oktext,$start,$states) = @_;

	my $level;
	
	my $out = '';
	if ($states->{'red'}) {
		$out .= $states->{'red'}.' errors';
		$level = 'red';
	}
	if ($states->{'yellow'}) {
		$out .= ( $out eq '' ? '' : ', ' ).$states->{'yellow'}.' warnings';
		$level = !$level ? 'yellow' : $level;
	}
	if ($states->{'unknown'}) {
		$out .= ( $out eq '' ? '' : ', ' ).$states->{'unknown'}.' unknown readings';
		$level = !$level ? 'unknown' : $level;
	}

	if ($out eq '') {
		return "<span class='green'>$oktext</span>";
	} else {
		return "$start<span class='$level'>$out</span>";
	}
	
}

sub status_html(\%)
{
	my ($status) = @_;

	my $out = html_head(htmlize($status->{name}), htmlize(text_state('',': ',%{$status->{states}})));

	$out .= '<div>'.htmlize($status->{fullname}). "</div>\n" if $status->{fullname};
	$out .= '<div>Time: '.$status->{time}."</div>\n";
	$out .= '<p>'.html_state('No problems detected','',%{$status->{states}})."</p>\n";

	my @groups;

	for my $type (keys %{$status->{sensors}}) {
		my $group = "\n<h2>".htmlize( $hwmap{$type} ? $hwmap{$type} : $type );
		$group .= html_state('',': ',%{$status->{sensorstates}->{$type}});
		$group .= "</h2>\n<ul>\n";

		foreach my $sensor (sort status_sort @{$status->{sensors}->{$type}}) {
			$group .= '<li class="' . $sensor->{state} . '">' . htmlize($sensor->{name});

			if ($sensor->{reading} && $sensor->{units} ne '') {
				$group .= ': ' . htmlize($sensor->{reading}) . ' ' . htmlize($sensor->{units});
			}

			if ($sensor->{operational}) {
				$group .= "\n<ul>\n";
				foreach (sort keys %{$sensor->{operational}}) {
					$group .= '<li>' . htmlize($_);
					$group .= ': ' . htmlize($sensor->{operational}->{$_}) if $sensor->{operational}->{$_};
					$group .= "</li>\n";
				}
				$group .= "</ul>\n";
			}

			$group .= "</li>\n";

		}
		$group .= "</ul>\n";

		push @groups, [$group, $type, $status->{worststate}->{$type}];
	}

	foreach my $group (sort group_sort @groups) {
		$out .= $group->[0];
	}

	$out .= html_tail();

	return $out;
}

sub send_status(\%)
{
	my ($status) = @_;

	my $server = Opts::get_option('mailserver');
	my $ssl = Opts::get_option('mailssl');
	my $sender = Opts::get_option('sender');
	my $recipient = Opts::get_option('recipient');
	my $mailuser = Opts::get_option('mailuser');
	my $mailpass = Opts::get_option('mailpass');
	my $html = Opts::get_option('mailhtml');

	return if (!$server);
	die "--sender and --recipient are required when using --server.\n" if (!$sender || !$recipient);
	die "Either both --mailuser and --mailpass have to be specified, or none of them.\n" if (($mailuser && !$mailpass) || (!$mailuser && $mailpass));

	return if (Opts::get_option('maillevel') && Opts::get_option('maillevel') > $status->{worst}); #only send important enough messages

	my $subject = 'ESXi report for '.$status->{name} .
	              text_state('',': ',%{$status->{states}});

	my $smtp = Net::SMTP->new($server, Timeout => 60, SSL => $ssl);

	die "Error setting up SMTP connection: $!\n" unless $smtp;

	$smtp->auth($mailuser,$mailpass) if $mailuser;
	$smtp->mail($sender);
	$smtp->recipient(split(/;/,$recipient));

	$smtp->data();
	$smtp->datasend('From: '.$sender."\n");
	$smtp->datasend('To: '.(split(/;/,$recipient))[0]."\n");
	$smtp->datasend('Subject: '.$subject."\n");
   	$smtp->datasend("MIME-Version: 1.0\n");

   	if ($html) {
   		$smtp->datasend("Content-Type: text/html; charset=utf-8\n");
	   	$smtp->datasend("\n");
   		$smtp->datasend(status_html(%{$status}));
   	} else {
   		$smtp->datasend("Content-Type: text/plain; charset=utf-8\n");
	   	$smtp->datasend("\n");
   		$smtp->datasend(status_text(%{$status}));
   	}

	my $result = $smtp->dataend();
	$smtp->quit;

	if(!$result) {
		print "Sending message failed.\n";
	} else {
		print "Report sent.\n";
	}
}

sub write_status($$$)
{
	my ($file,$content,$type) = @_;

	if ($file) {
		open REP,">$file" or die;
		print REP $content;
		close REP;
		print "$type report saved to $file.\n";
	}
}

Opts::add_options(%opts);
Opts::parse();
Opts::validate();
Util::connect();

main();

Util::disconnect();
