App::OpenVZ::BCWatch - Monitor the OpenVZ user_beancounters file


App-OpenVZ-BCWatch documentation Contained in the App-OpenVZ-BCWatch distribution.

Index


Code Index:

NAME

Top

App::OpenVZ::BCWatch - Monitor the OpenVZ user_beancounters file

SYNOPSIS

Top

 use App::OpenVZ::BCWatch;

 $watch = App::OpenVZ::BCWatch->new;
 $watch->process;

DESCRIPTION

Top

App::OpenVZ::BCWatch monitors the /proc/user_beancounters file and sends mail notifications whenever important values change (as defined by the user).

The recommended usage of this application module is to use the provided vzwatchd daemon.

Version 2.5 of the user_beancounters file is supported.

CONSTRUCTOR

Top

new

 $watch = App::OpenVZ::BCWatch->new;

Instantiate an App::OpenVZ::BCWatch object.

METHODS

Top

process

 $watch->process;

Inspect whether the user_beancounters file differs from the last run, and if, report the differences via mail.

DAEMON USAGE

Top

Run vzwatchd as root without arguments to obtain a list of commands.

CONFIGURATION FILE

Top

When vzwatchd is started initially, the /etc/vzwatchd.conf file will be generated. The configuration will then subsequently remain inactive until the _active flag is either set within or deleted from the configuration file.

Syntax

The configuration file is being parsed by Config::File. Unless noted otherwise, each option takes exactly one value. Specifying more than one value for an option requires that the values are separated by whitespace.

Options

Adjustable options with defaults are:

* mail[from]

Mail sender address. Defaults to root@localhost.

* mail[to]

Mail recipient address. Defaults to root@localhost.

* mail[subject]

Mail subject line. Defaults to: vzwatchd: NOTICE.

* monitor_fields

One or more field names to monitor for changes. Defaults to: failcnt.

* sleep

Sleep interval in seconds. Defaults to 60.

* verbose

Be verbose? Defaults to false.

* _active

Configuration active? Defaults to false.

Additional options with defaults are:

* input_file

Location of the user_beancounters file. Defaults to /proc/user_beancounters.

* data_file

Location of the data file. Defaults to $HOME/vzwatchd.dat.

CURRENT LIMITATIONS

Top

* Cannot parse anything else than the user_beancounters file.
* Only version 2.5 of the user_beancounters file is known to work.
* Fixed mail report template.
* No more than one mail recipient.

BUGS & CAVEATS

Top

Note that the sender and recipient mail address options with defaults might require adjustment in order for notifications to be delivered.

SEE ALSO

Top

OpenVZ::BC, http://openvz.org

AUTHOR

Top

Steven Schubiger <schubiger@cpan.org>

LICENSE

Top

This program is free software; you may redistribute it and/or modify it under the same terms as Perl itself.

See http://dev.perl.org/licenses/


App-OpenVZ-BCWatch documentation Contained in the App-OpenVZ-BCWatch distribution.

package App::OpenVZ::BCWatch;

use strict;
use warnings;
use boolean qw(true false);

use Carp qw(croak);
use File::Basename qw(basename);
use File::HomeDir ();
use File::Spec ();
use Mail::Sendmail qw(sendmail);
use Storable qw(store retrieve);
use Sys::Hostname qw(hostname);

our $VERSION = '0.03';

sub new
{
    my $class = shift;
    my %args = @_;

    my $defined_or = sub { defined $_[0] ? $_[0] : $_[1] };

    my $self = bless {
        Config => {
            input_file      => $args{input_file} || '/proc/user_beancounters',
            data_file       => $args{data_file}  || File::Spec->catfile(File::HomeDir->my_home, 'vzwatchd.dat'),
            _field_names    => [ qw(uid resource held maxheld barrier limit failcnt) ],
            _exclude_fields => [ qw(uid resource) ],
            monitor_fields  => $args{monitor_fields} || [ qw(failcnt) ],
            mail => {
                from    => $args{mail}->{from}    || 'root@localhost',
                to      => $args{mail}->{to}      || 'root@localhost',
                subject => $args{mail}->{subject} || 'vzwatchd: NOTICE',
            },
            sleep   => $defined_or->($args{sleep}, 60),
            verbose => $defined_or->($args{verbose}, false),
            _tests  => $defined_or->($args{_tests}, false),
        }
    }, ref($class) || $class;

    $self->_init;

    return $self;
}

sub process
{
    my $self = shift;

    delete @$self{qw(data stored)};

    $self->_get_data_running;
    $self->_get_data_file;
    $self->_compare_data;
    $self->_put_data_file;
}

sub _init
{
    my $self = shift;

    eval { store({}, $self->{Config}->{data_file}) }
      or croak "Cannot store to $self->{Config}->{data_file}: $!";

    my $pkg_tmpl = join '::', (__PACKAGE__, '_template');
    no strict 'refs';

    if (defined ${$pkg_tmpl}) {
        $self->{template} = ${$pkg_tmpl};
    }
    else {
        ${$pkg_tmpl} = $self->{template} = do {
            local $/ = '__END__';
            local $_ = <DATA>;
            chomp;
            s/^\s+//;
            s/\s+\z//;
            $_
        };
    }

    $self->{excluded} = {
        map {
          my $field = $_;
            (scalar grep $_ eq $field, @{$self->{Config}->{_exclude_fields}})
              ? ($field => true)
              : ($field => false)
        } @{$self->{Config}->{_field_names}}
    };

    my $i;
    $self->{index} = { map { $_ => $i++ } @{$self->{Config}->{_field_names}} };
}

sub _get_data_running
{
    my $self = shift;

    open(my $fh, '<', $self->{Config}->{input_file})
      or croak "Cannot read $self->{Config}->{input_file}: $!";
    my $output = do { local $/; <$fh> };
    close($fh);

    my $valid_format = join '\s+', @{$self->{Config}->{_field_names}};

    unless ($output =~ /$valid_format/) {
        croak "Format of $self->{Config}->{input_file} not recognized";
    }

    my $re = qr{
                                            \s*?
                  (?:\d+?\:)?  \s+?
                  (?:\w+?)     \s+?
                  (?:(?:\d+?)  \s*?){5}
        }x;

    my $uid;

    my @names = grep {
      !$self->{excluded}->{$_}
        ? $_ : ()
    } @{$self->{Config}->{_field_names}};

    local $1;
    while ($output =~ /^($re)$/gm) {
        my $line = $1;
        if ($line =~ /^ \s+? (\d+?)\:/gx) {
            $uid = $1;
        }
        my $res;
        if ($line =~ /\G \s+? (\w+)/gx) {
            $res = $1;
        }
        my @fields;
        if ($line =~ /\G \s+ (.*) $/x) {
            @fields = split /\s+/, $1;
        }
        push @{$self->{data}{$uid}{$res}},
          { map { $names[$_] => $fields[$_] } (0 .. $#fields) };
    }
}

sub _get_data_file
{
    my $self = shift;

    eval { $self->{stored} = retrieve($self->{Config}->{data_file}) }
      or croak "Cannot retrieve from $self->{Config}->{data_file}: $!";
}

sub _compare_data
{
    my $self = shift;

    my $has_changed = sub
    {
        my ($uid, $res, $i) = @_;

        my ($data, $stored) = map {
          my $type = $_; sub { $self->{$type}{$uid}{$res}->[$i]->{$_[0]} || 0 }
        } qw(data stored);

        return scalar grep { $data->($_) > $stored->($_) } @{$self->{Config}->{monitor_fields}};
    };

    foreach my $uid (keys %{$self->{stored}}) {
        foreach my $res (keys %{$self->{stored}{$uid}}) {
            foreach my $index (0 .. $#{$self->{stored}{$uid}{$res}}) {
                if ($has_changed->($uid, $res, $index)) {
                    $self->_create_report($uid, $res, $index);
                }
            }
        }
    }
}

sub _create_report
{
    my $self = shift;
    my ($uid, $res, $index) = @_;

    if ($self->{Config}->{_tests}) {
        push @{$self->{tests}->{report}}, $self->_prepare_report($uid, $res, $index);
    }
    else {
        my $report = $self->_prepare_report($uid, $res, $index);
        $self->_send_mail($report);

        if ($self->{Config}->{verbose}) {
            print "Report for \"$uid: $res\" sent to '$self->{Config}->{mail}->{to}'\n";
        }
    }
}

sub _put_data_file
{
    my $self = shift;

    eval { store($self->{data}, $self->{Config}->{data_file}) }
      or croak "Cannot store to $self->{Config}->{data_file}: $!";
}

sub _prepare_report
{
    my $self = shift;
    my ($uid, $res, $index) = @_;

    my @fixed_fields = ($uid, $res) x 2;
    my @mapping = (
        [
          { map {
            $_ => shift @fixed_fields,
          } @{$self->{Config}->{_exclude_fields}} },
          { map {
            $_ => $self->{stored}{$uid}{$res}->[$index]->{$_},
          } grep !$self->{excluded}->{$_}, @{$self->{Config}->{_field_names}} }
        ],
        [
          { map {
            $_ => shift @fixed_fields,
          } @{$self->{Config}->{_exclude_fields}} },
          { map {
            $_ => $self->{data}{$uid}{$res}->[$index]->{$_},
          } grep !$self->{excluded}->{$_}, @{$self->{Config}->{_field_names}} }
        ],
    );

    my @values;
    foreach my $map (@mapping) {
        my @v;
        foreach my $entry (@$map) {
            push @v, map $entry->{$_}, sort {
              $self->{index}->{$a} <=> $self->{index}->{$b}
            } keys %$entry;
        }
        push @values, [ @v ];
    }

    my %changed;
    foreach my $field (keys %{$mapping[0]->[1]}) {
        if ($mapping[0]->[1]->{$field} != $mapping[1]->[1]->{$field}) {
            $changed{$self->{index}->{$field}} = true;
        }
    }

    my $tmpl = \@values;
    my $report = $self->{template};

    local $1;

    while (my ($var) = $report =~ /(\$\S+)/) {
        unless ($report =~ /\Q$var\E$/m) {
            my $len = length($var) - length do { eval $var };
            $report =~ s/(?<=\Q$var\E)/' ' x $len/e;
        }
        $report =~ s/(\Q$var\E)/$1/ee;
    }

    while (my ($pos) = $report =~ /\((\d+)\)/) {
        my $marked = $changed{$pos} ? '*' : ' ';
        $report =~ s/\($pos\)/  $marked/;
    }

    return $report;
}

sub _send_mail
{
    my $self = shift;
    my ($report) = @_;

    my %mail = (
        From    => $self->{Config}->{mail}->{from},
        To      => $self->{Config}->{mail}->{to},
        Subject => $self->{Config}->{mail}->{subject},
        Message => <<EOT,
${\hostname}

$report

-- 
${\basename($0)} v$VERSION - ${\scalar localtime}
EOT
);
    sendmail(%mail)
      or croak "Cannot send mail: $Mail::Sendmail::error";
}

1;
__DATA__

Old:                          New:
----                          ----
Uid: $tmpl->[0][0]        (0) Uid: $tmpl->[1][0]
Res: $tmpl->[0][1]        (1) Res: $tmpl->[1][1]
Held: $tmpl->[0][2]       (2) Held: $tmpl->[1][2]
Maxheld: $tmpl->[0][3]    (3) Maxheld: $tmpl->[1][3]
Barrier: $tmpl->[0][4]    (4) Barrier: $tmpl->[1][4]
Limit: $tmpl->[0][5]      (5) Limit: $tmpl->[1][5]
Failcnt: $tmpl->[0][6]    (6) Failcnt: $tmpl->[1][6]

__END__