????
| Current Path : /usr/lib/sonarpush/ |
| Current File : //usr/lib/sonarpush/SonarPush.pm |
package SonarPush;
use strict;
use POSIX;
use Proc::Daemon;
use HTTP::Tiny;
use JSON::Tiny qw(decode_json encode_json);
use MIME::Base64;
use SonarPush::Globals;
use SonarPush::Providers::State;
use SonarPush::Providers::Hardware;
use SonarPush::Providers::Software;
=head1 NAME
SonarPush - Collects information/stats.
=head1 DESCRIPTION
Class for starting/running sonarpush daemon and collecting data about various machine
attributes like software, hardware, states. Once data is collected from the machine the
daemon is running on it is posted to a central server (currently Mr.Radar) where it is stored.
=head2 new
=over 4
=item Description:
my $sonarpush = SonarPush->new();
Initializes sonarpush object and creates a local datastructure of status attributes
used to keep track of success/fail attempts when posting the collected data to
the collection server. Provider objects are also initialized and stored in the local
$self->{providers} dataset which do the actual collection work.
=item Parameters
none
=item Returns
A blessed class object with status attributes, and provider classes.
=back
=cut
sub new {
my $class = shift;
my $self = {
status => {
Updates => {
successful => 0,
failed => 0,
total => 0,
},
Checkins => {
successful => 0,
failed => 0,
total => 0,
},
},
providers => {},
};
bless $self, $class;
map { $self->{providers}{$_} = "SonarPush::Providers::$_"->new($self) } qw(State Hardware Software);
$self->{Started} = POSIX::strftime("%Y/%m/%d %H:%M:%S", localtime);
return $self;
}
=head2 provider
=over 4
=item Description:
$self->provider('Software');
Creates or returns a previously created provider object. Each provider object contains methods used to collect data.
$self->provider('Hardware')->cpu;
=item Parameters
$provider: Software, Hardware, or State.
=item Returns
the provider object specified as the parameter.
=back
=cut
sub provider {
my ($self, $provider) = @_;
return $self->{providers}{$provider} ||= "SonarPush::Providers::$provider"->new($self);
}
=head2 userAgent
=over 4
=item Description:
$self->userAgent;
Creates or returns a previously created HTTP::Tiny UserAgent object.
=item Parameters
none
=item Returns
HTTP::Tiny UserAgent object
=back
=cut
sub userAgent {
my $self = shift;
my $version = $self->version;
$self->{useragent} ||= do {
my $ua = HTTP::Tiny->new(
agent => $SonarPush::Globals::USERAGENT . $version,
ssl_opts => { verify_hostname => 0, SSL_verify_mode => 0, },
timeout => $SonarPush::Globals::HTTP_TIMEOUT,
);
$ua;
};
return $self->{useragent};
}
=head2 post
=over 4
=item Description:
$self->post($args);
JSON encoded the passed parameters and submits a authenticated http post
of the encoded data.
=item Parameters
url parameters to pass to webserver, see process and run method.
=item Returns
returns a JSON encoded data returned by the webserver, either consisting of actual data, or error response codes.
=back
=cut
sub post {
my $self = shift;
my $args = shift;
my $json = encode_json($args);
my $ua = $self->userAgent;
my $response = $ua->post_form($SonarPush::Globals::PUSH_URL,
{ request => $json },
{ headers => { Authorization => $self->authHeader } },
);
if ($response->{status} == 599) {
$response->{error} .= "Error processing https request timeout [ $@ ]\n";
}
if (!($response->{success})) {
$response->{error} .= "Error processing https request: " . $response->{reason} . "\n";
}
else {
$self->printlog("Good POST\n");
my $body = $response->{content};
eval {
$response = decode_json($body);
};
if ($@) {
$response->{error} .= "Invalid JSON reponse received. $body\n";
}
else {
if ($response->{'RequestResult'} eq "OK") {
$self->printlog("Result = OK\n");
}
else {
$response->{error} .= 'Error processing https request ' + $response->{'RequestResultMessage'} . "\n";
}
}
}
return $response;
}
=head2 run
=over 4
=item Description:
$sonarpush->run()
After $sonarpush->start has been called successfully run() puts the program
into an endless timed loop and processes schedule info every X seconds.
=item Parameters
none
=item Returns
undef
=back
=cut
sub run {
my $self = shift;
my $args = shift;
if (($< == 0) or ($> == 0) or ($( == 0) or ($) == 0)) {
$self->suicide("Root privileges were not dropped properly. Exiting!\n");
}
$self->{failcount} = 0;
$self->{loopint} = $SonarPush::Globals::DEFAULT_COLLECTION_LOOP;
while (1) {
$self->checkDebugFile;
$0 = $self->name . " [getting schedule information]";
$self->printlog("Getting schedule information\n");
if ($self->{failcount} > 5) {
$self->printlog("More than 5 Consecutive HTTP failures have taken place. Falling back to fail-safe LoopInterval.\n");
$self->{loopint} = $SonarPush::Globals::DEFAULT_COLLECTION_LOOP;
}
$self->scheduler({ Command => 'getScheduleInfo' });
$0 = $self->name . " [sleeping]";
$self->printlog("Sleeping for $self->{loopint} seconds\n");
sleep($self->{loopint});
}
return;
}
=head2 scheduler
=over 4
=item Description:
$sonarpush->scheduler({ Command => 'getScheduleInfo' })
gets a list of tasks and passes those tasks to be processed.
keeps track of failure/success counts when processing those requests.
also keeps track of http post success and failures.
=item Parameters
{ Command => 'getScheduleInfo' }
=item Returns
$self
=back
=cut
sub scheduler {
my $self = shift;
my $request = shift;
$self->{status}{Checkins}{total}++;
my $response = $self->post($request);
if (!$response->{error}) {
$self->{status}{Checkins}{successful}++;
$self->process($response);
}
else {
$self->{status}{Checkins}{failed}++;
$self->{failcount}++;
$self->printlog($response->{error}, 1);
}
return $self;
}
=head2 process
=over 4
=item Description:
$self->process($response);
this method determines if colletion providers need to be executed or not,
=item Parameters
The response to a successful $self->post({ Command => 'getScheduleInfo' });
=item Returns
$self
=back
=cut
sub process {
my $self = shift;
my $response = shift;
if ($response->{'NextTask'} > $response->{'LoopInterval'}) {
$self->printlog(
"NextTask ("
. $response->{'NextTask'}
. ") is larger than our Sonar-specified LoopInterval ("
.
$response->{'LoopInterval'} . "). Setting LoopInterval to Sonar-specified LoopInterval.\n"
);
$self->{loopint} = $response->{'LoopInterval'};
}
elsif ($response->{'NextTask'} < 5) {
$self->printlog("NextTask (" . $response->{'NextTask'} . ") is less than 5 seconds away. Setting LoopInterval to 5 seconds.\n");
$self->{loopint} = 5;
}
else {
$self->printlog("Setting LoopInterval to make our next request coincide with NextTask.\n");
$self->{loopint} = ($response->{'NextTask'} + 1);
}
if ($response->{'TasksWaiting'} eq "yes") {
$0 = $self->name . " [collecting data]";
$self->printlog("Performing collections for the following tasks: " . $response->{'TaskList'} . "\n");
my $buffer = $self->_generateTaskXML($response->{TaskList});
$0 = $self->name . " [submitting data]";
$self->printlog("Submitting data.\n");
$self->update({
Command => 'postcollecteddata',
XMLdata => $buffer,
WorkTicketID => $response->{'WorkTicketID'},
});
}
return $self;
}
=head2 update
=over 4
=item Description:
$self->update({
Command => 'postcollecteddata',
XMLdata => $buffer,
WorkTicketID => $response->{'WorkTicketID'},
});
this method posts xml encoded provider task data to be posted to the collector server.
also keeps track fo successful/fail updates posted to the collection server.
=item Parameters
Command => 'postcollecteddata
XMLdata => valid xml string,
WorkTicketID => derived from scheduleinfo.
=item Returns
$self
=back
=cut
sub update {
my $self = shift;
my $request = shift;
$self->{status}{Updates}{total}++;
my $result = $self->post($request);
if ($result->{error}) {
$self->{failcount}++;
$self->{status}{Updates}{failed}++;
$self->printlog($result->{error}, 1);
}
else {
$self->{failcount} = 0;
$self->{status}{Updates}{successful}++;
}
return $self;
}
sub checkDebugFile {
my ($self) = @_;
if ((-f $SonarPush::Globals::DEBUGFLAGFILE) and !$self->{debug}) {
$self->{debug} = 1;
$self->printlog("DEBUGFLAGFILE found. Enabling debug mode.\n");
}
elsif ((!-f $SonarPush::Globals::DEBUGFLAGFILE) and ($self->{debug})) {
$self->printlog("DEBUGFLAGFILE is gone and DEBUG is currently set. Disabling debug mode.\n");
$self->{debug} = 0;
}
}
=head2 start
=over 4
=item Description:
$sonarpush->start($args);
this method starts the program as a process and forks, sets appropriate perms on the process
pid file,
=item Parameters
any options that may be passed to Proc::Daemon for process initialzation.
=item Returns
nothing
=back
=cut
sub start {
my $self = shift;
my $args = shift;
if ($args->{debug}) {
$self->{debug} = delete $args->{debug};
$self->printlog("Debug mode... staying in foreground.\n");
}
if (($> != 0) or ($< != 0)) {
$self->suicide("This program requires root privileges.\n");
}
$0 = $self->name . " [starting up]";
$self->printlog("SonarPush " . $self->version . " starting\n");
$self->setProcessUser;
my $daemon;
if (!$self->{debug}) {
$args->{pid_file} = $SonarPush::Globals::PIDFile;
$args->{child_STDERR} = "+>>$SonarPush::Globals::LOGFILEERROR";
my $daemon = Proc::Daemon->new();
$daemon->Init($args);
$SIG{'HUP'} = sub {
$self->handleHUP();
};
chown $self->{processuser}{uid}, $self->{processuser}{gid}, $SonarPush::Globals::PIDFile
or $self->printlog("Warning: Could not change ownership on PID File!\n", 1);
chmod 0664, $SonarPush::Globals::PIDFile
or $self->printlog("Warning: Could not change permissions on PID File!\n", 1);
chown $self->{processuser}{uid}, $self->{processuser}{gid}, $self->getlogfilename()
or $self->printlog("Warning: Could not set permissions on log file!\n");
}
my $credentials = $self->_authCredentials;
$credentials->{auth_user} || $self->suicide("Error loading UserName for push authentication. Exiting.\n");
if (!$credentials->{auth_pass}) {
$self->register;
}
$self->dropPrivs({
uid => $self->{processuser}{uid},
gid => $self->{processuser}{gid}
});
$self->run();
exit(1);
}
=head2 setProcessUser
=over 4
=item Description:
this method sets the UID of the running process to the user specified in Globals.pm,
=item Parameters
none
=item Returns
$self
=back
=cut
sub setProcessUser {
my $self = shift;
$self->{processuser} ||= do {
(my $login, my $pass, my $newUID, my $newGID) = getpwnam($SonarPush::Globals::NonPrivUser)
or $self->suicide("$SonarPush::Globals::NonPrivUser not in passwd file");
($login, $pass) = undef;
{
uid => $newUID,
gid => $newGID
};
};
if (!$self->{processuser}{uid} || !$self->{processuser}{gid}) {
$self->suicide("Error obtaining non privledged user information!\n");
}
return $self;
}
=head2 dropPrivs
=over 4
=item Description:
this program is started with root privs, and this method drops those privs to the UID
that is derived in setProcessUser.
=item Parameters
{ $uid => 112, gid => 115}
=item Returns
$self
=back
=cut
sub dropPrivs {
my $self = shift;
my $args = shift;
if ($args->{uid} == 0) {
$self->suicide("New UID has root privileges. Exiting!\n");
}
if ($args->{gid} == 0) {
$self->suicide("New GID has root privileges. Exiting!\n");
}
$self->printlog("UID (real/effective): $</$>\n");
$self->printlog("GID (real/effective): $(/$)\n");
$self->printlog("Dropping privileges...\n");
$( = $args->{gid};
$) = $args->{gid};
$< = $args->{uid};
$> = $args->{uid};
$self->printlog("UID (real/effective): $</$>\n");
$self->printlog("GID (real/effective): $(/$)\n");
return $self;
}
=head2 register
=over 4
=item Description:
before sonarpush can collect data it must register with the collection server, this method
will continuously try to register with the UNIQ_ID of the subaccnt until a success is received.
A sucessful registration means the app was able to successfully download a password file and store it.
=item Parameters
none
=item Returns
$self
=back
=cut
sub register {
my $self = shift;
my $tryfail = 1;
my $regtries = 0;
my $maxregtries = 5;
while (($regtries <= $maxregtries) and $tryfail) {
$regtries++;
$self->printlog("Attempting to perform registration with Sonar system (attempt $regtries of $maxregtries).\n");
my %options = (
headers => {
Authorization => $self->authHeader($SonarPush::Globals::REGISTER_USER, $SonarPush::Globals::REGISTER_PASS),
}
);
my $ua = $self->userAgent;
my $res = $ua->get("$SonarPush::Globals::REGISTER_URL?uid=$self->{auth_user}", \%options);
if (!($res->{success})) {
# GET failed
$self->printlog("Request failed: [ $res->{status} $res->{reason} ].\n", 1);
$tryfail = 1;
}
else {
my $body = $res->{content};
# Creation: SUCCESS
# Password: SUCCESS: tEXnZG4gKzZLpQYBl754JHnDYBHZa
if ($body =~ /^Creation: SUCCESS$/m) {
# Creation was good
$self->printlog("Base registration successful.\n");
if ($body =~ /^Password: SUCCESS: ([A-Za-z0-9]{24,32})$/m) {
$self->printlog("Password successfully retrieved.\n");
$tryfail = 0;
my $success = $self->storeAuthPass($1);
if (not $success) {
$self->suicide("There was an error writing to the password file.\n");
}
}
else {
# Password retrieval failed
$self->suicide("Password retrieval failed. Password will need to be configured manually.\n");
}
}
else {
# Server creation unsuccessful
$self->printlog("Base registration unsuccessful.\n");
$tryfail++;
}
}
if ($tryfail) {
my $sleeptime = 600 * $regtries;
$self->printlog("Sleeping for $sleeptime before retry...\n", 1);
sleep($sleeptime);
}
}
return $self;
}
=head2 _deriveTaskList
=over 4
=item Description:
this method orders tasks by the provider it belogs to derived from the string of tasks returned by the collection server
=item Parameters
a comma seperated string of tasks to be executed, this is returned after a successful getScheduleInfo post.
=item Returns
a hashref of key value pairs of provider => tasks:
{
software => [apache, exim],
hardware => [cpu disk],
state => [network],
}
=back
=cut
sub _deriveTasklist {
my $self = shift;
my $response = shift;
my %tasks = map { $_ => 1 } split(/,/, $response);
for my $alias (qw(EVERYTHING ALLSOFTWARE ALLSTATE ALLHARDWARE)) {
if ($tasks{$alias}) {
delete $tasks{$alias};
for my $subTask (@{ $SonarPush::Globals::TaskList{$alias} }) {
$tasks{$subTask} = 1;
}
}
}
my %taskData;
TASK: foreach my $task (sort keys %tasks) {
foreach my $type (qw/software hardware state/) {
for my $typeTask (@{ $SonarPush::Globals::TaskList{ 'ALL' . uc $type } }) {
if ($typeTask eq $task) {
$taskData{$type} ||= [];
push(@{ $taskData{$type} }, $task);
next TASK;
}
}
}
}
return \%taskData;
}
=head2 _generateTaskXML
=over 4
=item Description:
executes the provider tasks, and creates a wellformed XML string.
=item Parameters
a comma seperated string of tasks to be executed, this is returned after a successful getScheduleInfo post.
=item Returns
an xml string
=back
=cut
sub _generateTaskXML {
my $self = shift;
my $response = shift;
my $providerAttrs = {
providedby => 'NONE',
datatype => 'None',
};
# This particular line of raw XML isn't producible by the XML generator.
# As such, we need to explicitly prepend it to the generated output.
# This is the only case where we should need to write explicit XML
my @buffer = ('<?xml version="1.0"?>');
my $taskhash = $self->_deriveTasklist($response);
my $dataNode = {
name => 'data',
value => [],
};
foreach my $provider (sort keys %$taskhash) {
my $providerNode = {
name => $provider,
attributes => $providerAttrs,
value => [],
};
push(@{ $dataNode->{value} }, $providerNode);
my $class = ucfirst($provider);
foreach my $task (@{ $taskhash->{$provider} }) {
$task =~ s/[^\w_]//g;
push(@{ $providerNode->{value} }, $self->provider($class)->$task());
}
}
push(@buffer, SonarPush::Providers::encodeXML({
name => 'provider',
value => [ { name => 'version', value => 'SonarPush ' . $self->version }, $dataNode ]
}));
return join("\n", @buffer);
}
sub _authCredentials {
my $self = shift;
return {
auth_user => $self->authUsername,
auth_pass => $self->authPassword,
};
}
sub storeAuthPass {
my $self = shift;
my $auth_pass = shift;
my $success = 1;
my $fh;
open $fh, '>', $SonarPush::Globals::PASSWORD_FILE or $success = 0;
if ($success) {
print $fh $auth_pass;
chown 0, 0, $SonarPush::Globals::PASSWORD_FILE;
chmod 0600, $SonarPush::Globals::PASSWORD_FILE;
$self->{auth_pass} = $auth_pass;
}
return $success;
}
sub authUsername {
my $self = shift;
my $username;
$self->{auth_user} ||= do {
my $flag = 1;
my $input;
{
local $/;
open(INPUT, "< $SonarPush::Globals::USERNAME_FILE") or $flag = 0;
if ($flag) {
$input = <INPUT>;
}
}
if ($flag) {
if ($input =~ /^([A-Za-z0-9]{1,32})\s+$/) {
$username = $1;
}
}
$username;
};
return $self->{auth_user};
}
sub name {
my $self = shift;
$self->{appname} ||= do {
my $app = "SonarPush " . $self->version;
$app;
};
return $self->{appname};
}
sub version {
my $self = shift;
my $version;
$self->{version} ||= do {
if (-e $SonarPush::Globals::VERSIONFILE) {
if (-r $SonarPush::Globals::VERSIONFILE) {
local $/;
open(INPUT, "< $SonarPush::Globals::VERSIONFILE") or $self->suicide("Could not open version file: $!");
$version = <INPUT>;
close(INPUT);
}
}
chomp $version;
$version;
};
return $self->{version};
}
sub authPassword {
my $self = shift;
my $password;
$self->{auth_pass} ||= do {
my $flag = 1;
my $input;
if (-e $SonarPush::Globals::PASSWORD_FILE) {
if (-r $SonarPush::Globals::PASSWORD_FILE) {
# Password file exists and is readable
local $/;
open(INPUT, "< $SonarPush::Globals::PASSWORD_FILE") or $flag = 0;
if ($flag) {
$input = <INPUT>;
}
}
else {
#password file exists, but is not readable
$self->suicide("Password file exists, but is not readable. Exiting.\n");
}
if ($flag and $input =~ /^([A-Za-z0-9]{1,32})\s*$/) {
$password = $1;
}
}
$password;
};
return $self->{auth_pass};
}
sub now {
my $self = shift;
my $now = POSIX::strftime("[%Y/%m/%d %H:%M:%S] ", localtime);
return $now;
}
sub getlogfilename {
my $self = shift;
return $SonarPush::Globals::LOGFILEBASE . POSIX::strftime("%Y%m%d", localtime) . ".log";
}
sub suicide {
my $self = shift;
my $message = shift;
$self->printlog($message, 1);
die $message;
}
sub printlog {
my $self = shift;
my $message = shift;
my $error = shift;
print $message;
my $logfile = $error ? $SonarPush::Globals::LOGFILEERROR : $self->getlogfilename();
if ($error or $self->{debug}) {
open(my $file, '>>', $logfile);
print $file $self->now() . $message;
}
return;
}
sub handleHUP {
my $self = shift;
print "Caught HUP signal... \n";
print "Removing PID file $SonarPush::Globals::PIDFile... ";
if (unlink $SonarPush::Globals::PIDFile) {
print "success!\n";
}
else {
print "failed!\n";
}
exit;
}
sub authHeader {
my ($self, $authuser, $authpass) = @_;
$authuser ||= $self->authUsername;
$authpass ||= $self->authPassword;
my $code = 'Basic ' . MIME::Base64::encode($authuser . ':' . $authpass);
$code =~ s/\n$//;
return $code;
}
1;