File: //proc/self/root/opt/fastcomet/nightwatch/Fastcomet/Nightwatch/Util.pm
package Fastcomet::Nightwatch::Util;
=head1 Perl Artistic License
Copyright 2013 Linode, LLC. Longview is made available under the terms
of the Perl Artistic License, or GPLv2 at the recipients discretion.
=head2 Perl Artistic License
Read it at L<http://dev.perl.org/licenses/artistic.html>.
=head2 GNU General Public License (GPL) Version 2
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, see http://www.gnu.org/licenses/
See the full license at L<http://www.gnu.org/licenses/>.
=cut
use strict;
use warnings;
use Exporter 'import';
our @EXPORT = qw($logger);
our %EXPORT_TAGS = (
DRIVER => [qw(constant_push flatten_data slurp_file daemonize_self check_already_running $SLEEP_TIME enable_debug_logging $VERSION post $logger $apikey)],
BASIC => [qw(slurp_file $PROCFS $logger)],
SYSINFO => [qw(slurp_file detect_system $VERSION $PROCFS $ARCH $logger)],
APPLICATIONS => [qw(get_config_file_name get_config_data application_error application_preflight $logger)],
);
our @EXPORT_OK
= qw(slurp_file detect_system constant_push flatten_data
get_config_file_name get_config_data application_error application_preflight
daemonize_self check_already_running enable_debug_logging post get_UA
$PROCFS $ARCH $SLEEP_TIME $TICKS $VERSION $apikey);
use Fastcomet::Nightwatch::Logger;
use Fastcomet::Nightwatch::STDERRLogger;
use Fastcomet::Linux::Distribution qw(distribution_name);
use Carp;
use FindBin;
use POSIX;
use JSON;
use LWP::UserAgent;
use HTTP::Request;
our $logger = get_logger();
our $gua;
our $VERSION = '1.0.2';
our $TICKS = POSIX::sysconf(&POSIX::_SC_CLK_TCK);
our $PROCFS = find_procfs() or $logger->logdie("Couldn't find procfs: $!");
our $ARCH = get_architecture() or $logger->info("Couldn't determine architecture: $!");
our $SLEEP_TIME = 60;
our $apikey;
my $pid_file = '/var/run/nightwatch.pid';
my $conf_path = '/etc/fastcomet/nightwatch.d/';
my $global_conf_path = "$FindBin::RealBin/../config";
my $slots = 10;
my %push_iteration;
sub get_UA {
return $gua if defined $gua;
$gua = LWP::UserAgent->new(
timeout => 10,
agent => "Fastcomet Nightwatch $VERSION",
ssl_opts => {MultiHomed => 1, Timeout => 10}
);
return $gua;
}
sub post {
my $payload = shift;
my $ua = get_UA();
local $SIG{ALRM} = sub { die "LWP-Timeout";};
alarm(180);
$logger->trace("Posting Data");
my $config_file = get_config_data("$global_conf_path/main.conf");
unless ( exists( $config_file->{post_target} ) ) {
$logger->warn(
"Unable to find the POST target in the configuration"
);
return;
}
my $req = $ua->request(HTTP::Request->new('POST', $config_file->{post_target}, [
'Authorization' =>"Bearer $apikey",
'Content-Type' => 'application/json',
'Content-Encoding' => 'gzip'
], Compress::Zlib::memGzip(encode_json($payload))));
alarm(0);
return $req;
}
sub get_logger {
return $logger if defined $logger;
$logger = Fastcomet::Nightwatch::Logger->new($levels->{info});
return $logger;
}
sub enable_debug_logging {
$logger->level($levels->{trace});
}
# For an arbitrarily sized cache ($slots long), we want to remove an element to keep the
# average gap between any two elements as small as possible. Turns out that the right
# sequence to remove elements is a repeating sequence of runs of 1 .. $slots - 2, with
# ($slots -1) x ((2**runNumber) - 1) interleaved before each element of the run.
#
# The first push below handles the inserting ($slots -1) x ((2**runNumber) - 1), while
# the second adds current element of the run. The loop is a while rather than a for,
# as the length of the list becomes MUCH longer than the iteration currently being
# calculated, after just a few runs (~2), so it's easier to measure the length of
# the sequence directly than try to approximate it from the iteration number
sub remove_sequence {
my $iteration = shift;
my $current = 0;
my @sequence;
while (scalar(@sequence) < $iteration) {
# We never want to remove element 0, so candidates for removal are 1 through $slots - 1
# $slots - 1 will be inserted before the beginning of each element starting in the second run,
# so each run should cover the range 1 to $slots - 2. You can calculate which run you're on
# by doing the integer division of $current by the top element of a run (ie $slots -2).
push @sequence, $slots-1 for(1..(2**int($current/($slots-2)))-1);
push @sequence,($current%($slots-2))+1;
$current++;
}
@sequence = @sequence[0 .. $iteration-1];
return @sequence if (wantarray);
return $sequence[-1];
}
# A nice wrapper to memoize and hide the complexities of removeSequence when pushing on to a fixed sized cache
sub constant_push {
my ($ar,$val) = @_;
$logger->debug('Array Ref is undefined') unless (defined($ar));
my $addr = substr $ar,6,9;
$push_iteration{$addr} = 0 if(scalar(@$ar)==0);
push @$ar,$val;
# Remove sequence expects to start at 1, while the first removal doesn't need to happen until $slots
# so we subtract ($slots -1) to keep the numbers lined up with what each side expects
splice(@$ar,remove_sequence($push_iteration{$addr}-($slots-1)),1) if (scalar(@$ar) > $slots);
$push_iteration{$addr}++;
}
sub slurp_file {
my $path = shift;
open( my $fh, '<', $path ) or return;
return <$fh> if wantarray;
chomp(my $data = join( '', <$fh> ));
return $data;
}
sub detect_system {
my @cpu = slurp_file( $PROCFS . 'cpuinfo' );
if (
( slurp_file(
'/sys/devices/system/clocksource/clocksource0/current_clocksource'
) eq 'kvm-clock'
)
|| ( ( grep {/QEMU Virtual CPU/} @cpu )
&& ( grep {/hypervisor/} @cpu ) ) )
{
return 'kvm';
}
elsif ( (-f $PROCFS . 'user_beancounters') && (distribution_name() ne 'CloudLinux') ) {
return 'openvz';
}
elsif ( -e '/dev/vzfs' ) {
return 'virtuozzo';
}
elsif ( -d '/sys/bus/xen' ) {
return 'xen';
}
return 'baremetal';
}
sub flatten_data {
my ($mlhr,$name) = @_;
my $ret = {};
for my $sk (keys %{$mlhr}) {
if ((ref $mlhr->{$sk}) eq 'HASH'){
my $children = flatten_data($mlhr->{$sk}, $sk);
for my $child (keys %{$children}) {
my $k = $name ? $name . "." . $child : $child;
$ret->{$k} = $children->{$child};
}
}
else {
my $k = $name ? $name . "." . $sk : $sk;
$ret->{$k} = $mlhr->{$sk};
}
}
return $ret;
}
sub get_architecture {
return ( ( POSIX::uname() )[4] );
}
sub find_procfs {
return "/proc/" if -d "/proc/$$";
my @mtab = grep {/\bproc\b/} ( slurp_file('/etc/mtab') )
or do{
$logger->info("Couldn't check /etc/mtab: $!");
return undef;
};
return ( split( /\s+/, $mtab[0] ) )[1] . '/';
}
sub daemonize_self {
#<<< perltidy ignore
chdir '/' or $logger->logdie("Can't chdir to /: $!");
open STDIN, '<', '/dev/null' or $logger->logdie("Can't read /dev/null: $!");
open STDOUT, '>>', '/dev/null' or $logger->logdie("Can't write to /dev/null: $!");
open STDERR, '>>', '/dev/null' or $logger->logdie("Can't write to /dev/null: $!");
tie *STDERR, "Fastcomet::Nightwatch::STDERRLogger";
defined( my $pid = fork ) or $logger->logdie("Can't fork: $!");
exit if $pid;
setsid or $logger->logdie("Can't start a new session: $!");
umask 022;
system "echo $$ > $pid_file";
#>>>
}
sub check_already_running {
return 0 unless (-e $pid_file);
my $pid = slurp_file($pid_file);
return 0 unless -e $PROCFS . "$pid/cmdline";
my $name = slurp_file($PROCFS . "$pid/cmdline");
return $pid if $name =~ /nightwatch/i;
return 0;
}
sub get_config_file_name {
my $caller = shift || caller ;
$caller =~ s/.*:://;
return $conf_path . $caller . '.conf';
}
sub get_config_data {
my $file = shift;
my $ret = {};
open my $fh, '<', $file or do {
$logger->warn("Unable to open $file: $!");
return $ret;
};
while (my $line = <$fh>) {
next if $line =~ /^\s*#/;
next if $line =~ /^\s*$/;
my ($key, $value) = $line =~ /^\s*(\S*)\s+(\S*)\s*$/;
unless ($key && $value) {
$logger->error("Unable to parse line in $file: '$line' does not conform to standard");
next;
}
$ret->{$key} = $value;
}
return $ret;
}
sub application_error {
my ($dataref, $namespace, $message, $code) = @_;
$logger->error($message);
$dataref->{applications}->{$namespace}->{status} = $code;
$dataref->{applications}->{$namespace}->{message} = $message;
return $dataref;
}
sub application_preflight {
my ($dataref, $signatures, $config_file) = @_;
my ($found, @appkeys) = (undef, grep {/^Processes\./} keys (%{$dataref->{LONGTERM}}));
for my $sig (@$signatures) {
if (grep {m/\Q.$sig./} @appkeys) {
$found = 1;
last;
}
}
unless ($found) {
for my $longname (grep {/^Processes\..*\.longname/} keys (%{$dataref->{INSTANT}})){
for my $sig (@$signatures) {
if($dataref->{INSTANT}->{$longname} =~ /\b$sig\b/){
$found = 1;
last;
}
}
}
}
return unless $found;
return 1 unless -e $config_file;
unless ( ((stat($config_file))[2] & 00007) == 0) {
$logger->logdie("$config_file should not be world-accessible. If you're unsure of which permissions you prefer, please run 'chown root:root $config_file && chmod 400 $config_file'");
}
return 1;
}
1;