summaryrefslogtreecommitdiff
path: root/lib/MON/RESTlos.pm
diff options
context:
space:
mode:
Diffstat (limited to 'lib/MON/RESTlos.pm')
-rw-r--r--lib/MON/RESTlos.pm471
1 files changed, 471 insertions, 0 deletions
diff --git a/lib/MON/RESTlos.pm b/lib/MON/RESTlos.pm
new file mode 100644
index 0000000..d13ecce
--- /dev/null
+++ b/lib/MON/RESTlos.pm
@@ -0,0 +1,471 @@
+package MON::RESTlos;
+
+use strict;
+use warnings;
+use v5.10;
+use autodie;
+
+use POSIX 'strftime';
+use IO::File;
+use IO::Dir;
+use HTTP::Headers;
+use LWP::UserAgent;
+use Data::Dumper;
+
+use MON::Cache;
+use MON::Config;
+use MON::Display;
+use MON::Filter;
+use MON::Utils;
+use MON::JSON;
+
+our @ISA = ('MON::Display');
+
+sub new {
+ my ( $class, %opts ) = @_;
+
+ my $self = bless \%opts, $class;
+
+ $self->init();
+
+ return $self;
+}
+
+sub init {
+ my ($self) = @_;
+
+ my $config = $self->{config};
+
+ my $host = $config->get('restlos.api.host');
+ my $port = $config->get('restlos.api.port');
+ my $protocol = $config->get('restlos.api.protocol');
+
+ $self->{url_base} = "$protocol://$host:$port/";
+ $self->{cache} = MON::Cache->new( config => $config );
+ $self->{filter} = MON::Filter->new( config => $config );
+ $self->{json} = MON::JSON->new( config => $config );
+ $self->{has_error} = 0;
+ $self->{had_error} = 0;
+
+ my $url = $self->{url_base};
+ my $vals = $self->{json}->decode( $self->fetch_json($url) );
+
+ my $all = $self->{all} = $vals->{endpoints};
+ my @top;
+ push @top, $_ for sort keys %$all;
+ $self->{all_possible_paths} = \@top;
+
+ return undef;
+}
+
+# Easy getter methods
+sub get_possible_paths {
+ my ($self) = @_;
+
+ return $self->{all_possible_paths};
+}
+
+sub get_path_params {
+ my ( $self, $path ) = @_;
+
+ return $self->{all}{$path};
+}
+
+# Helper methods
+sub set_credentials {
+ my ( $self, $ua ) = @_;
+
+ my $config = $self->{config};
+
+ my $host = $config->get('restlos.api.host');
+ my $port = $config->get('restlos.api.port');
+ my $protocol = $config->get('restlos.api.protocol');
+ my $password = $config->get_maybe_encoded('restlos.auth.password');
+ my $realm = $config->get('restlos.auth.realm');
+ my $username = $config->get('restlos.auth.username');
+
+ $ua->credentials( "$host:$port", $realm, $username, $password );
+
+ return undef;
+}
+
+sub create_request {
+ my ( $self, $method, $url ) = @_;
+
+ my $req = HTTP::Request->new( $method, $url );
+ $req->header( 'Accept', 'application/json' );
+ $req->header( 'Content-Type', 'application/json' );
+
+ return $req;
+}
+
+sub handle_http_error_if {
+ my ( $self, $response ) = @_;
+
+ my $config = $self->{config};
+
+ unless ( $response->is_success() ) {
+
+ #$self->out_json( $response->decoded_content() );
+ $self->warning( $response->status_line() . ' ==> switching to dry mode' );
+ $self->{has_error} = 1;
+ $self->{had_error} = 1;
+ }
+ else {
+ $self->{has_error} = 0;
+ }
+
+ return undef;
+}
+
+# Fetch methods
+sub fetch_json {
+ my ( $self, $url ) = @_;
+
+ my $config = $self->{config};
+ my $cache = $self->{cache};
+
+ my $response = $cache->magic(
+ $url,
+ sub {
+ $self->verbose("Requesting '$url' via GET");
+
+ my $req = $self->create_request( 'GET', $url );
+
+ my $ua = LWP::UserAgent->new();
+ $self->set_credentials($ua);
+
+ my $response = $ua->request($req);
+ $self->handle_http_error_if($response);
+
+ return $response;
+ }
+ );
+
+ return $response->decoded_content();
+}
+
+sub fetch_path_json {
+ my ( $self, $path, $params ) = @_;
+
+ my $config = $self->{config};
+ my $filter = $self->{filter};
+ $filter->compute($params);
+
+ my $content =
+ $self->fetch_json( $self->{url_base} . $path . $filter->{query_string} );
+
+ return $self->{json}
+ ->encode( $filter->filter( $self->{json}->decode($content) ) );
+}
+
+# Delete methods
+sub delete_json {
+ my ( $self, $url ) = @_;
+
+ my $config = $self->{config};
+ my $filter = $self->{filter};
+
+ if ( $config->{'dry'} ) {
+ $self->verbose("Dry mode, don't modify anything via API.");
+ return undef;
+ }
+
+ $self->verbose("Requesting '$url' via DELETE");
+ my $req = $self->create_request( 'DELETE', $url );
+
+ my $ua = LWP::UserAgent->new();
+ $self->set_credentials($ua);
+
+ my $response = $ua->request($req);
+ $self->handle_http_error_if($response);
+
+ return $response->decoded_content();
+}
+
+sub delete_path_json {
+ my ( $self, $path, $params, $no_backup ) = @_;
+
+ my $config = $self->{config};
+ my $filter = $self->{filter};
+ my $json = $self->{json};
+
+ if ( $config->{'dry'} ) {
+ $self->verbose("Dry mode, don't modify anything via API.");
+ return undef;
+ }
+
+ $filter->compute($params);
+ $self->backup_path_json( $path, $params ) unless defined $no_backup;
+
+ if ( $filter->{num_filters} > 0 ) {
+ my $jsonstr = $self->fetch_path_json( $path, $params );
+ my $data = $json->decode($jsonstr);
+ my @ret;
+
+ for my $obj (@$data) {
+ my $url = $self->{url_base} . $path . "?name.eq=$obj->{name}";
+ push @ret, $json->decode( $self->delete_json($url) );
+ }
+
+ return $json->encode( \@ret );
+
+ }
+ else {
+ my $url = $self->{url_base} . $path . $filter->{query_string};
+ return $self->delete_json($url);
+ }
+}
+
+# Post methods
+sub send_json {
+ my ( $self, $url, $send_data, $method ) = @_;
+
+ $method //= 'POST';
+
+ my $config = $self->{config};
+
+ if ( $config->{'dry'} ) {
+ $self->verbose("Dry mode, don't modify anything via API.");
+ return undef;
+ }
+
+ $send_data = '' unless defined $send_data;
+
+ $self->verbose("Using URL $url and $method data:\n$send_data");
+
+ my $req = $self->create_request( $method, $url );
+ $req->content($send_data);
+
+ my $ua = LWP::UserAgent->new();
+ $self->set_credentials($ua);
+
+ my $response = $ua->request($req);
+ $self->handle_http_error_if($response);
+
+ return $response->decoded_content();
+}
+
+sub send_path_json {
+ my ( $self, $path, $send_data, $no_backup, $method ) = @_;
+
+ # If $method == undef, then $method = 'POST'
+
+ my $config = $self->{config};
+
+ if ( $config->{'dry'} ) {
+ $self->verbose("Dry mode, don't modify anything via API.");
+ return undef;
+ }
+
+ my $url = $self->{url_base} . $path;
+ $self->backup_path_json($path) unless defined $no_backup;
+
+ return $self->send_json( $url, $send_data, $method );
+}
+
+# Post methods
+sub post_verify_json {
+ my ($self) = @_;
+
+ my $config = $self->{config};
+
+ if ( $config->{'dry'} ) {
+ $self->verbose("Dry mode, don't modify anything via API.");
+ return undef;
+ }
+
+ $self->info("Verifying configuration.");
+ return $self->send_json( $self->{url_base} . 'control?verify' );
+}
+
+sub post_restart_json {
+ my ($self) = @_;
+
+ my $config = $self->{config};
+
+ if ( $config->{'dry'} ) {
+ $self->verbose("Dry mode, don't modify anything via API.");
+ return undef;
+ }
+
+ $self->info("Restarting monitoring core.");
+ return $self->send_json( $self->{url_base} . 'control?restart=true' );
+}
+
+# Allow variables like this:
+# m -v update host set __FOO = '$host_name $name' where host_name like paul
+sub vars {
+ my ( $self, $elem, $v ) = @_;
+
+ $v =~ s/\\\$/:ESCAPE_DOLLAR/g;
+ $v =~ s/\\@/:ESCAPE_AT/g;
+ $v =~ s/\@(\w+)/\$$1/g;
+ $v =~ s/\@(\{\w+\})/\$$1/g;
+
+ my @vars1 = $v =~ /\$(\w+)/g;
+ my @vars2 = $v =~ /\$\{(\w+)\}/g;
+
+ $v =~ s/\$\{(\w+)\}/\$$1/g;
+ $v =~ s/\\\$/\$/g;
+
+ for ( @vars1, @vars2 ) {
+ unless ( exists $elem->{$_} ) {
+ my @possible = map { "\$$_" } keys %$elem;
+ $self->error(
+ "Variable \$$_ (aka \@$_) does not exist. Possible: @possible");
+ }
+
+ $self->verbose("Evaluating variable '\$$_' to '$elem->{$_}'");
+ $v =~ s/\$$_/$elem->{$_}/;
+ }
+
+ $v =~ s/:ESCAPE_DOLLAR/\$/g;
+ $v =~ s/:ESCAPE_AT/\@/g;
+
+ return $v;
+}
+
+# Update methods
+sub update_path_json {
+ my ( $self, $path, $params, $set ) = @_;
+
+ my $config = $self->{config};
+ my $filter = $self->{filter};
+
+ if ( $config->{'dry'} ) {
+ $self->verbose("Dry mode, don't modify anything via API.");
+ return undef;
+ }
+
+ $filter->compute($params);
+ my $url = $self->{url_base} . $path . $filter->{query_string};
+
+ my $json = $self->fetch_path_json( $path, $params );
+
+ $self->backup_path_json( $path, $params, $json );
+ my $vals = $self->{json}->decode($json);
+
+ for my $elem (@$vals) {
+ while ( my ( $k, $v ) = each %$set ) {
+ $elem->{$k} = $self->vars( $elem, $v );
+ }
+ }
+
+ $json = $self->{json}->encode($vals);
+
+ return $self->send_path_json( $path, $json, 1 );
+}
+
+sub update_remove_path_json {
+ my ( $self, $path, $params, $remove ) = @_;
+
+ my $config = $self->{config};
+
+ if ( $config->{'dry'} ) {
+ $self->verbose("Dry mode, don't modify anything via API.");
+ return undef;
+ }
+
+ my $json = $self->fetch_path_json( $path, $params );
+
+ $self->backup_path_json( $path, $params, $json );
+ my $vals = $self->{json}->decode($json);
+
+ for my $removekey (@$remove) {
+ my $flag = 0;
+
+ for my $elem (@$vals) {
+ if ( exists $elem->{$removekey} ) {
+ delete $elem->{$removekey};
+ $flag = 1;
+ }
+ }
+
+ $self->warning("No key '$removekey' to remove found.") unless $flag;
+ }
+
+ $json = $self->{json}->encode($vals);
+ return $self->send_path_json( $path, $json, 1, 'PUT' );
+}
+
+# Backup methods
+sub backup_cleanup {
+ my ( $self, $path, $params ) = @_;
+
+ my $config = $self->{config};
+ my $location = $config->get('backups.dir');
+
+ if ( $config->{'dry'} ) {
+ $self->verbose(
+ "Dry mode, don't modify anything via API, backup irrelevant.");
+ return undef;
+ }
+
+ my $dir = IO::Dir->new($location);
+
+ if ( defined $dir ) {
+ my $days = $config->get('backups.keep.days');
+
+ while ( defined( $_ = $dir->read() ) ) {
+ my $backfile = "$location/$_";
+ my $age = -M $backfile;
+
+ #$self->verbose("'$backfile' has age $age");
+ if ( $backfile =~ /backup_.*\.json/ && $days <= $age ) {
+ $self->verbose("Deleting '$backfile', it's older than $days days");
+ unlink $backfile;
+ }
+ }
+
+ $dir->close();
+ }
+
+ return undef;
+}
+
+sub backup_path_json {
+ my ( $self, $path, $params, $json ) = @_;
+
+ my $config = $self->{config};
+
+ if ( $config->{'dry'} ) {
+ $self->verbose(
+ "Dry mode, don't modify anything via API, backup irrelevant.");
+ return undef;
+ }
+
+ return undef if $config->bool('backups.disable');
+
+ my $days = $config->get('backups.keep.days');
+ my $location = $config->get('backups.dir');
+
+ unless ( -d $location ) {
+ $self->info("Creating '$location' for backups");
+ $self->info("Backups older than $days days will be automatically deleted");
+ mkdir $location;
+ }
+
+ my $backfile =
+ $location . strftime( "/backup_%Y%m%d_%H%M%S_$path.json", localtime );
+
+ #$self->info("To rollback run: $0 post $path < $backfile");
+
+ my $fh = IO::File->new( $backfile, 'w' );
+ $self->error("Could not open file $backfile for writing a backup")
+ unless defined $fh;
+
+ unless ( defined $json ) {
+ $self->verbose("Retrieving data for backup");
+ $json = $self->fetch_path_json( $path, $params );
+ }
+
+ print $fh $json;
+
+ $fh->close();
+ $self->backup_cleanup();
+
+ return undef;
+}
+
+1;