summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2015-01-02 14:00:56 +0100
committerPaul Buetow <paul@buetow.org>2015-01-02 14:00:56 +0100
commit4f27f3ea59baa6cfca0ac1df96b1dfedbd83706c (patch)
tree79754e3c49216f80cb3ad5579851fca0bf1a31c9
initial
-rw-r--r--Makefile61
-rw-r--r--README.md9
-rw-r--r--ca.pem1
-rw-r--r--contrib/zsh/_mon.zsh46
-rw-r--r--debian/README6
-rw-r--r--debian/changelog5
-rw-r--r--debian/compat1
-rw-r--r--debian/control16
-rw-r--r--debian/copyright30
-rw-r--r--debian/files1
-rw-r--r--debian/mon.debhelper.log45
-rw-r--r--debian/mon.manpages1
-rw-r--r--debian/mon.substvars2
-rw-r--r--debian/mon/DEBIAN/control12
-rw-r--r--debian/mon/DEBIAN/md5sums21
-rwxr-xr-xdebian/mon/usr/bin/m133
-rwxr-xr-xdebian/mon/usr/bin/mi3
-rwxr-xr-xdebian/mon/usr/bin/mon133
-rw-r--r--debian/mon/usr/share/doc/mon/changelog.gzbin0 -> 170 bytes
-rw-r--r--debian/mon/usr/share/doc/mon/copyright30
-rw-r--r--debian/mon/usr/share/man/man1/mon.1.gzbin0 -> 6617 bytes
-rw-r--r--debian/mon/usr/share/mon/contrib/zsh/_mon.zsh46
-rw-r--r--debian/mon/usr/share/mon/examples/mon.conf.sample23
-rw-r--r--debian/mon/usr/share/mon/itca.pem23
-rw-r--r--debian/mon/usr/share/mon/lib/MAPI/Cache.pm55
-rw-r--r--debian/mon/usr/share/mon/lib/MAPI/Config.pm176
-rw-r--r--debian/mon/usr/share/mon/lib/MAPI/Display.pm360
-rw-r--r--debian/mon/usr/share/mon/lib/MAPI/Filter.pm166
-rw-r--r--debian/mon/usr/share/mon/lib/MAPI/JSON.pm51
-rw-r--r--debian/mon/usr/share/mon/lib/MAPI/Options.pm163
-rw-r--r--debian/mon/usr/share/mon/lib/MAPI/Query.pm557
-rw-r--r--debian/mon/usr/share/mon/lib/MAPI/QueryBase.pm232
-rw-r--r--debian/mon/usr/share/mon/lib/MAPI/RESTlos.pm471
-rw-r--r--debian/mon/usr/share/mon/lib/MAPI/Syslogger.pm77
-rw-r--r--debian/mon/usr/share/mon/lib/MAPI/Utils.pm80
-rw-r--r--debian/mon/usr/share/mon/version1
-rwxr-xr-xdebian/rules13
-rw-r--r--docs/README.txt1
-rw-r--r--docs/mon.1546
-rw-r--r--docs/mon.1.gzbin0 -> 6625 bytes
-rw-r--r--docs/mon.pod409
-rw-r--r--docs/mon.txt394
-rw-r--r--lib/MON/Cache.pm55
-rw-r--r--lib/MON/Config.pm176
-rw-r--r--lib/MON/Display.pm360
-rw-r--r--lib/MON/Filter.pm166
-rw-r--r--lib/MON/JSON.pm51
-rw-r--r--lib/MON/Options.pm163
-rw-r--r--lib/MON/Query.pm557
-rw-r--r--lib/MON/QueryBase.pm232
-rw-r--r--lib/MON/RESTlos.pm471
-rw-r--r--lib/MON/Syslogger.pm77
-rw-r--r--lib/MON/Utils.pm80
-rwxr-xr-xmi3
-rwxr-xr-xmon133
-rw-r--r--mon.conf23
-rw-r--r--redhat-specs-add.txt2
-rw-r--r--t/Makefile2
-rw-r--r--t/delete.t28
-rw-r--r--t/edit.t26
-rw-r--r--t/get.t25
-rw-r--r--t/get_interactive.t25
-rw-r--r--t/get_where.t48
-rw-r--r--t/getfmt.t23
-rw-r--r--t/insert.t29
-rw-r--r--t/options_debug_verbose_quiet.t43
-rw-r--r--t/options_version.t28
-rw-r--r--t/post.t41
-rw-r--r--t/post_as_array.t39
-rw-r--r--t/post_interactive.t41
-rw-r--r--t/put.t60
-rw-r--r--t/update.t49
-rw-r--r--t/update_format.t37
-rw-r--r--t/view.t22
74 files changed, 7515 insertions, 0 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..7c3dd66
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,61 @@
+NAME=mon
+SHORTNAME=m
+all: version documentation perltidy
+version:
+ cut -d' ' -f2 debian/changelog | head -n 1 | sed 's/(//;s/)//' > .version
+perltidy:
+ find . -name \*.pm | xargs perltidy -i 2 -b
+ perltidy -b $(NAME)
+ find . -name \*.bak -delete
+documentation:
+ pod2man --release="$(NAME) $$(cat .version)" \
+ --center="User Commands" ./docs/$(NAME).pod > ./docs/$(NAME).1
+ pod2text ./docs/$(NAME).pod > ./docs/$(NAME).txt
+ gzip -c ./docs/$(NAME).1 > ./docs/$(NAME).1.gz
+install: deinstall
+ test ! -d $(DESTDIR)/usr/bin && mkdir -p $(DESTDIR)/usr/bin || exit 0
+ test ! -d $(DESTDIR)/usr/share/$(NAME) && mkdir -p $(DESTDIR)/usr/share/$(NAME) || exit 0
+ test ! -d $(DESTDIR)/usr/share/$(NAME)/examples && mkdir -p $(DESTDIR)/usr/share/$(NAME)/examples || exit 0
+ test ! -d $(DESTDIR)/usr/share/$(NAME)/contrib && mkdir -p $(DESTDIR)/usr/share/$(NAME)/contrib || exit 0
+ test ! -d $(DESTDIR)/usr/share/man/man1 && mkdir -p $(DESTDIR)/usr/share/man/man1 || exit 0
+ cp $(NAME) $(DESTDIR)/usr/bin
+ cp $(NAME) $(DESTDIR)/usr/bin/$(SHORTNAME)
+ cp ./mi $(DESTDIR)/usr/bin
+ cp -r ./lib $(DESTDIR)/usr/share/$(NAME)/
+ cp ./.version $(DESTDIR)/usr/share/$(NAME)/version
+ cp ./$(NAME).conf $(DESTDIR)/usr/share/$(NAME)/examples/$(NAME).conf.sample
+ cp ./ca.pem $(DESTDIR)/usr/share/$(NAME)/ca.pem
+ cp -R ./contrib/* $(DESTDIR)/usr/share/$(NAME)/contrib
+ cp ./docs/$(NAME).1.gz $(DESTDIR)/usr/share/man/man1/$(NAME).1.gz
+ test ! -z "$(DESTDIR)" && find $(DESTDIR) -name .\*.swp -delete || exit 0
+deinstall:
+ test -f $(DESTDIR)/usr/bin/$(NAME) && rm $(DESTDIR)/usr/bin/$(NAME) && rm $(DESTDIR)/usr/bin/$(SHORTNAME) && rm $(DESTDIR)/usr/bin/mi || exit 0
+ test -d $(DESTDIR)/usr/share/$(NAME) && rm -r $(DESTDIR)/usr/share/$(NAME) || exit 0
+ test -f $(DESTDIR)/usr/share/man/man1/$(NAME).1.gz && rm -f $(DESTDIR)/usr/share/man/man1/$(NAME).1.gz || exit 0
+uninstall: deinstall
+clean:
+ test -d debian/$(NAME)/usr && rm -Rf debian/$(NAME)/usr || exit 0
+dch:
+ dch -i
+deb:
+ dpkg-buildpackage -uc -us
+rpm: deb
+ alien -r --generate ../$(NAME)_$$(cat ./.version)_all.deb
+ cp redhat-specs-add.txt rpm.specs
+ sed "s/%%{ARCH}/noarch/" $$(pwd)/$(NAME)-$$(cat ./.version)/*.spec >> rpm.specs
+ mv rpm.specs $$(pwd)/$(NAME)-$$(cat ./.version)/*.spec
+ rpmbuild --buildroot $$(pwd)/$(NAME)-$$(cat ./.version) -ba $$(pwd)/$(NAME)-$$(cat ./.version)/*.spec
+release: dch version
+ git commit -a -m 'New release'
+ bash -c "git tag $$(cat .version)"
+ git push --tags
+ git push origin master
+clean-top:
+ rm -f ../$(NAME)_*.tar.gz || exit 0
+ rm -f ../$(NAME)_*.dsc || exit 0
+ rm -f ../$(NAME)_*.changes || exit 0
+ rm -f ../$(NAME)_*.deb || exit 0
+ rm -f ../$(NAME)-*.rpm || exit 0
+dotest:
+ sh -c 'cd ./t;make'
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..2b213c1
--- /dev/null
+++ b/README.md
@@ -0,0 +1,9 @@
+mon
+====
+
+The humble monitoring API tool.
+
+This tool assists you to manage monitoring configurations via the
+RESTlos API.
+
+Please checkout manual pages at ./docs or man mon.
diff --git a/ca.pem b/ca.pem
new file mode 100644
index 0000000..7fe007a
--- /dev/null
+++ b/ca.pem
@@ -0,0 +1 @@
+I am invalid, fill me
diff --git a/contrib/zsh/_mon.zsh b/contrib/zsh/_mon.zsh
new file mode 100644
index 0000000..3770c60
--- /dev/null
+++ b/contrib/zsh/_mon.zsh
@@ -0,0 +1,46 @@
+#compdef m mon
+
+get_options() {
+ elements=`${words[1,$[${CURRENT}-1]]} --dry --nocolor 2>&1`
+ if [ $? -ne 0 ]; then
+ elements=""
+ fi
+
+ echo `echo $elements | tr '\n' ' '`
+}
+
+_mon() {
+ local curcontext="$curcontext" state line
+ typeset -A opt_args
+
+ case $words[2] in
+ getfmt)
+ if [ $CURRENT -eq 5 ]; then
+ compadd "$@" "where"
+ return
+ fi
+ ;;
+ post|put)
+ if [ $CURRENT -eq 4 ]; then
+ compadd "$@" "from"
+ return
+ fi
+ ;;
+ delete|edit|get|insert|update|view)
+ if [ $CURRENT -eq 4 ]; then
+ compadd "$@" "where"
+ return
+ fi
+ ;;
+ esac
+
+ VALUES="`get_options`"
+ if [ -z "${VALUES}" ]; then
+ _files
+ else
+ ARGS=(${=VALUES})
+ compadd "$@" ${ARGS[@]}
+ fi
+}
+
+_mon "$@"
diff --git a/debian/README b/debian/README
new file mode 100644
index 0000000..41fc005
--- /dev/null
+++ b/debian/README
@@ -0,0 +1,6 @@
+The Debian Package mon
+----------------------------
+
+This is just a humble monitoring api tool.
+
+ -- Paul Buetow <paul@buetow.org> Sun, 08 Apr 2012 15:23:53 +0200
diff --git a/debian/changelog b/debian/changelog
new file mode 100644
index 0000000..22d063a
--- /dev/null
+++ b/debian/changelog
@@ -0,0 +1,5 @@
+mon (1.0.0) stable; urgency=low
+
+ * Initial
+
+ -- Paul Buetow <paul@buetow.org> Fri, 02 Jan 2015 13:50:50 +0100
diff --git a/debian/compat b/debian/compat
new file mode 100644
index 0000000..45a4fb7
--- /dev/null
+++ b/debian/compat
@@ -0,0 +1 @@
+8
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..9d5d0e0
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,16 @@
+Source: mon
+Section: utils
+Priority: optional
+Maintainer: Paul Buetow <paul@buetow.org>
+Build-Depends: perl, perltidy, alien, rpm
+Standards-Version: 3.9.2
+Homepage: http://mon.buetow.org
+Vcs-Git: https://github.com/rantanplan/mon
+Vcs-Browser: https://github.com/rantanplan/mon
+
+Package: mon
+Architecture: all
+Depends: ${perl:Depends}, libwww-perl, libtime-modules-perl, libconfig-json-perl, libio-socket-ssl-perl, libunix-syslog-perl, libjson-xs-perl, libterm-readline-gnu-perl
+Recommends: ca-root-cert, vim
+Description: A simple monitoring API tool
+ Uses the RESTlos monitoring API to configure monitoring stuff.
diff --git a/debian/copyright b/debian/copyright
new file mode 100644
index 0000000..9be5c7f
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,30 @@
+Format: http://dep.debian.net/deps/dep5
+Upstream-Name: mon
+Source: http://mon.buetow.org
+
+Files: *
+Copyright: 2012 Paul Buetow <paul@buetow.org>
+License: GPL-3.0+
+
+Files: debian/*
+Copyright: 2012 Paul Buetow <paul@buetow.org>
+License: GPL-3.0+
+
+License: GPL-3.0+
+ 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 3 of the License, or
+ (at your option) any later version.
+ .
+ This package 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/>.
+ .
+ On Debian systems, the complete text of the GNU General
+ Public License version 3 can be found in "/usr/share/common-licenses/GPL-3".
+
+
diff --git a/debian/files b/debian/files
new file mode 100644
index 0000000..1e9ea35
--- /dev/null
+++ b/debian/files
@@ -0,0 +1 @@
+mon_1.0.0_all.deb utils optional
diff --git a/debian/mon.debhelper.log b/debian/mon.debhelper.log
new file mode 100644
index 0000000..545a50f
--- /dev/null
+++ b/debian/mon.debhelper.log
@@ -0,0 +1,45 @@
+dh_auto_configure
+dh_auto_build
+dh_auto_test
+dh_prep
+dh_installdirs
+dh_auto_install
+dh_install
+dh_installdocs
+dh_installchangelogs
+dh_installexamples
+dh_installman
+dh_installcatalogs
+dh_installcron
+dh_installdebconf
+dh_installemacsen
+dh_installifupdown
+dh_installinfo
+dh_pysupport
+dh_installinit
+dh_installmenu
+dh_installmime
+dh_installmodules
+dh_installlogcheck
+dh_installlogrotate
+dh_installpam
+dh_installppp
+dh_installudev
+dh_installwm
+dh_installxfonts
+dh_installgsettings
+dh_bugfiles
+dh_ucf
+dh_lintian
+dh_gconf
+dh_icons
+dh_perl
+dh_usrlocal
+dh_link
+dh_compress
+dh_fixperms
+dh_installdeb
+dh_gencontrol
+dh_md5sums
+dh_builddeb
+dh_builddeb
diff --git a/debian/mon.manpages b/debian/mon.manpages
new file mode 100644
index 0000000..202840a
--- /dev/null
+++ b/debian/mon.manpages
@@ -0,0 +1 @@
+docs/mon.1
diff --git a/debian/mon.substvars b/debian/mon.substvars
new file mode 100644
index 0000000..bcb0957
--- /dev/null
+++ b/debian/mon.substvars
@@ -0,0 +1,2 @@
+perl:Depends=perl
+misc:Depends=
diff --git a/debian/mon/DEBIAN/control b/debian/mon/DEBIAN/control
new file mode 100644
index 0000000..35e9094
--- /dev/null
+++ b/debian/mon/DEBIAN/control
@@ -0,0 +1,12 @@
+Package: mon
+Version: 3.0.1
+Architecture: all
+Maintainer: Paul Buetow <paul@buetow.org>
+Installed-Size: 126
+Depends: perl, libwww-perl, libtime-modules-perl, libconfig-json-perl, libio-socket-ssl-perl, libunix-syslog-perl, libjson-xs-perl, libterm-readline-gnu-perl
+Recommends: ca-root-cert, vim
+Section: utils
+Priority: optional
+Homepage: https://mamat-git0101.infra.server.lan/mon
+Description: A simple monitoring API tool
+ Uses the RESTlos monitoring API to configure monitoring stuff.
diff --git a/debian/mon/DEBIAN/md5sums b/debian/mon/DEBIAN/md5sums
new file mode 100644
index 0000000..54cd36a
--- /dev/null
+++ b/debian/mon/DEBIAN/md5sums
@@ -0,0 +1,21 @@
+8397dbe590d346eb3b925331eb5c2db1 usr/bin/m
+8397dbe590d346eb3b925331eb5c2db1 usr/bin/mon
+504f6e72ac6e115d63d3e9a1ccd1288c usr/bin/mi
+e7abd63037f4be7013488c671a4716dd usr/share/doc/mon/changelog.gz
+f125cc11bbc34b49bbfc518d07a96213 usr/share/doc/mon/copyright
+9d4f77923a3c90cd6c5e2fe2136db9b6 usr/share/man/man1/mon.1.gz
+a93997a0ddabf9ca06e602cca2134ea4 usr/share/mon/contrib/zsh/_mon.zsh
+fc1844f15efdefc185d67518ae4857c2 usr/share/mon/examples/mon.conf.sample
+7b124b0a3bc0bc716d3434f994fa7498 usr/share/mon/ca.pem
+84722dc1495b5dba22d013eacba87ee8 usr/share/mon/lib/MON/Cache.pm
+2c1750e42e80ba05c59af0b7a15f2407 usr/share/mon/lib/MON/Config.pm
+752936b2d93398123c8d2238f8f9c9db usr/share/mon/lib/MON/Display.pm
+442663ada73b39675e6e3a0012ad268a usr/share/mon/lib/MON/Filter.pm
+330f96c524cd1c0dc012d94c7e216201 usr/share/mon/lib/MON/JSON.pm
+f6dab5e42b4367a1af832cd76fc2b92b usr/share/mon/lib/MON/Options.pm
+581f3d813169e433953fa761efb555fd usr/share/mon/lib/MON/Query.pm
+ce6a905ad17647d4b7a3bfe268ac9b73 usr/share/mon/lib/MON/QueryBase.pm
+5baa39a120e8a8d66d38e9490e93dd17 usr/share/mon/lib/MON/RESTlos.pm
+6c8278360c0676c7c0f208b1d884128c usr/share/mon/lib/MON/Syslogger.pm
+b62cea4e2ffd5b449f10e8b209fc151d usr/share/mon/lib/MON/Utils.pm
+662f52dba3387be714becad581d0ac87 usr/share/mon/version
diff --git a/debian/mon/usr/bin/m b/debian/mon/usr/bin/m
new file mode 100755
index 0000000..162f71a
--- /dev/null
+++ b/debian/mon/usr/bin/m
@@ -0,0 +1,133 @@
+#!/usr/bin/perl
+#
+# (c) 2013, 2014 1&1 Internet AG
+# Paul C. Buetow <paul@buetow.org>
+
+use strict;
+use warnings;
+use v5.10;
+use autodie;
+
+use Data::Dumper;
+use Term::ReadLine;
+
+$| = 1;
+my $lib;
+
+BEGIN {
+ if ( -d './lib/MON' ) {
+ $lib = './lib';
+
+ }
+ else {
+ $lib = '/usr/share/mon/lib';
+ }
+
+ unless ( exists $ENV{HTTPS_CA_FILE} ) {
+ if ( -f 'ca.pem' ) {
+ $ENV{HTTPS_CA_FILE} = 'ca.pem';
+ }
+ elsif ( -f '/etc/ssl/certs/ca.pem' ) {
+ $ENV{HTTPS_CA_FILE} = '/etc/ssl/certs/ca.pem';
+ }
+ else {
+ $ENV{HTTPS_CA_FILE} = '/usr/share/mon/ca.pem';
+ }
+ }
+}
+
+use lib $lib;
+
+use MON::RESTlos;
+use MON::Config;
+use MON::Options;
+use MON::Query;
+use MON::Syslogger;
+use MON::Utils;
+use MON::Display;
+
+sub main (@) {
+ my @args = @_;
+ my @opts;
+
+ # Only interpret OPTIONS if they are at the beginning
+ push @opts, shift @args while @args and $args[0] =~ /^-/;
+
+ # .. or at the end
+ push @opts, pop @args while @args and $args[-1] =~ /^-/;
+
+ my $options = MON::Options->new( opts_passed => \@opts );
+ my $logger = MON::Syslogger->new( options => $options );
+ my $config = MON::Config->new( options => $options, logger => $logger );
+
+ if ( $config->{'version'} ) {
+ print get_version(), "\n";
+ exit 0;
+ }
+
+ $logger->logg( 'info', "Invoked by $ENV{USER} (params: @ARGV)" );
+
+ if ( $config->{interactive} ) {
+ my $term = Term::ReadLine->new('Monitoring API tool');
+ my $prompt = '>> ';
+ my $out = $term->OUT || \*STDOUT;
+
+ say $out "Welcome to the Monitoring API Tool v" . get_version();
+ say $out "Press Ctrl+D to exit; Prefix cmd with ! to run via shell";
+
+ while ( defined( $_ = $term->readline($prompt) ) ) {
+ $term->addhistory($_) if /\S/;
+
+ if (s/^!//) {
+ system($_);
+ }
+ else {
+
+ my $line = $_;
+
+ my @args = split / +/, $line;
+
+ my $api = MON::RESTlos->new( config => $config );
+ my $query = MON::Query->new(
+ config => $config,
+ api => $api,
+ options => $options,
+ args => \@args
+ );
+ $query->parse();
+ }
+ }
+
+ }
+ else {
+ my $api = MON::RESTlos->new( config => $config );
+ my $query = MON::Query->new(
+ config => $config,
+ api => $api,
+ options => $options,
+ args => \@args
+ );
+
+ $query->parse();
+ $query->verbose("Good bye");
+
+ if ( $api->{had_error} != 0 ) {
+
+ # Needed by Puppet to re-try operation the next Puppet run
+ if ( $config->{errfile} ne '' ) {
+ open my $fh, $config->{errfile} or die "$!: $config->{errfile}";
+ print $fh "Exited with an error\n";
+ close $fh;
+ }
+
+ exit 2;
+ }
+ else {
+ unlink $config->{errfile}
+ if $config->{errfile} ne '' and -f $config->{errfile};
+ exit 0;
+ }
+ }
+}
+
+main @ARGV;
diff --git a/debian/mon/usr/bin/mi b/debian/mon/usr/bin/mi
new file mode 100755
index 0000000..49efcc0
--- /dev/null
+++ b/debian/mon/usr/bin/mi
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+exec mon --meta --interactive $@
diff --git a/debian/mon/usr/bin/mon b/debian/mon/usr/bin/mon
new file mode 100755
index 0000000..162f71a
--- /dev/null
+++ b/debian/mon/usr/bin/mon
@@ -0,0 +1,133 @@
+#!/usr/bin/perl
+#
+# (c) 2013, 2014 1&1 Internet AG
+# Paul C. Buetow <paul@buetow.org>
+
+use strict;
+use warnings;
+use v5.10;
+use autodie;
+
+use Data::Dumper;
+use Term::ReadLine;
+
+$| = 1;
+my $lib;
+
+BEGIN {
+ if ( -d './lib/MON' ) {
+ $lib = './lib';
+
+ }
+ else {
+ $lib = '/usr/share/mon/lib';
+ }
+
+ unless ( exists $ENV{HTTPS_CA_FILE} ) {
+ if ( -f 'ca.pem' ) {
+ $ENV{HTTPS_CA_FILE} = 'ca.pem';
+ }
+ elsif ( -f '/etc/ssl/certs/ca.pem' ) {
+ $ENV{HTTPS_CA_FILE} = '/etc/ssl/certs/ca.pem';
+ }
+ else {
+ $ENV{HTTPS_CA_FILE} = '/usr/share/mon/ca.pem';
+ }
+ }
+}
+
+use lib $lib;
+
+use MON::RESTlos;
+use MON::Config;
+use MON::Options;
+use MON::Query;
+use MON::Syslogger;
+use MON::Utils;
+use MON::Display;
+
+sub main (@) {
+ my @args = @_;
+ my @opts;
+
+ # Only interpret OPTIONS if they are at the beginning
+ push @opts, shift @args while @args and $args[0] =~ /^-/;
+
+ # .. or at the end
+ push @opts, pop @args while @args and $args[-1] =~ /^-/;
+
+ my $options = MON::Options->new( opts_passed => \@opts );
+ my $logger = MON::Syslogger->new( options => $options );
+ my $config = MON::Config->new( options => $options, logger => $logger );
+
+ if ( $config->{'version'} ) {
+ print get_version(), "\n";
+ exit 0;
+ }
+
+ $logger->logg( 'info', "Invoked by $ENV{USER} (params: @ARGV)" );
+
+ if ( $config->{interactive} ) {
+ my $term = Term::ReadLine->new('Monitoring API tool');
+ my $prompt = '>> ';
+ my $out = $term->OUT || \*STDOUT;
+
+ say $out "Welcome to the Monitoring API Tool v" . get_version();
+ say $out "Press Ctrl+D to exit; Prefix cmd with ! to run via shell";
+
+ while ( defined( $_ = $term->readline($prompt) ) ) {
+ $term->addhistory($_) if /\S/;
+
+ if (s/^!//) {
+ system($_);
+ }
+ else {
+
+ my $line = $_;
+
+ my @args = split / +/, $line;
+
+ my $api = MON::RESTlos->new( config => $config );
+ my $query = MON::Query->new(
+ config => $config,
+ api => $api,
+ options => $options,
+ args => \@args
+ );
+ $query->parse();
+ }
+ }
+
+ }
+ else {
+ my $api = MON::RESTlos->new( config => $config );
+ my $query = MON::Query->new(
+ config => $config,
+ api => $api,
+ options => $options,
+ args => \@args
+ );
+
+ $query->parse();
+ $query->verbose("Good bye");
+
+ if ( $api->{had_error} != 0 ) {
+
+ # Needed by Puppet to re-try operation the next Puppet run
+ if ( $config->{errfile} ne '' ) {
+ open my $fh, $config->{errfile} or die "$!: $config->{errfile}";
+ print $fh "Exited with an error\n";
+ close $fh;
+ }
+
+ exit 2;
+ }
+ else {
+ unlink $config->{errfile}
+ if $config->{errfile} ne '' and -f $config->{errfile};
+ exit 0;
+ }
+ }
+}
+
+main @ARGV;
diff --git a/debian/mon/usr/share/doc/mon/changelog.gz b/debian/mon/usr/share/doc/mon/changelog.gz
new file mode 100644
index 0000000..89f05ae
--- /dev/null
+++ b/debian/mon/usr/share/doc/mon/changelog.gz
Binary files differ
diff --git a/debian/mon/usr/share/doc/mon/copyright b/debian/mon/usr/share/doc/mon/copyright
new file mode 100644
index 0000000..9be5c7f
--- /dev/null
+++ b/debian/mon/usr/share/doc/mon/copyright
@@ -0,0 +1,30 @@
+Format: http://dep.debian.net/deps/dep5
+Upstream-Name: mon
+Source: http://mon.buetow.org
+
+Files: *
+Copyright: 2012 Paul Buetow <paul@buetow.org>
+License: GPL-3.0+
+
+Files: debian/*
+Copyright: 2012 Paul Buetow <paul@buetow.org>
+License: GPL-3.0+
+
+License: GPL-3.0+
+ 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 3 of the License, or
+ (at your option) any later version.
+ .
+ This package 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/>.
+ .
+ On Debian systems, the complete text of the GNU General
+ Public License version 3 can be found in "/usr/share/common-licenses/GPL-3".
+
+
diff --git a/debian/mon/usr/share/man/man1/mon.1.gz b/debian/mon/usr/share/man/man1/mon.1.gz
new file mode 100644
index 0000000..1639eb5
--- /dev/null
+++ b/debian/mon/usr/share/man/man1/mon.1.gz
Binary files differ
diff --git a/debian/mon/usr/share/mon/contrib/zsh/_mon.zsh b/debian/mon/usr/share/mon/contrib/zsh/_mon.zsh
new file mode 100644
index 0000000..3770c60
--- /dev/null
+++ b/debian/mon/usr/share/mon/contrib/zsh/_mon.zsh
@@ -0,0 +1,46 @@
+#compdef m mon
+
+get_options() {
+ elements=`${words[1,$[${CURRENT}-1]]} --dry --nocolor 2>&1`
+ if [ $? -ne 0 ]; then
+ elements=""
+ fi
+
+ echo `echo $elements | tr '\n' ' '`
+}
+
+_mon() {
+ local curcontext="$curcontext" state line
+ typeset -A opt_args
+
+ case $words[2] in
+ getfmt)
+ if [ $CURRENT -eq 5 ]; then
+ compadd "$@" "where"
+ return
+ fi
+ ;;
+ post|put)
+ if [ $CURRENT -eq 4 ]; then
+ compadd "$@" "from"
+ return
+ fi
+ ;;
+ delete|edit|get|insert|update|view)
+ if [ $CURRENT -eq 4 ]; then
+ compadd "$@" "where"
+ return
+ fi
+ ;;
+ esac
+
+ VALUES="`get_options`"
+ if [ -z "${VALUES}" ]; then
+ _files
+ else
+ ARGS=(${=VALUES})
+ compadd "$@" ${ARGS[@]}
+ fi
+}
+
+_mon "$@"
diff --git a/debian/mon/usr/share/mon/examples/mon.conf.sample b/debian/mon/usr/share/mon/examples/mon.conf.sample
new file mode 100644
index 0000000..3cb6460
--- /dev/null
+++ b/debian/mon/usr/share/mon/examples/mon.conf.sample
@@ -0,0 +1,23 @@
+# Mandatory
+restlos.api.host: mamat-monitoringapi.infra.server.lan
+
+# Optional, default value is 'https'
+restlos.api.protocol: https
+
+# Optional, default value is '443'
+restlos.api.port: 443
+
+# Optional, default value is $USER
+restlos.auth.username: USERNAME
+
+# Mandatory
+restlos.auth.password.enc: PASSWORDBASE64ENCODED
+
+# Optional, default value is '1'
+backups.disable: 1
+
+# Optional, default value is '7'
+backups.keep.days: 0.1
+
+# Optional, default value is '~/.mon'
+backups.dir: ~/.mon
diff --git a/debian/mon/usr/share/mon/itca.pem b/debian/mon/usr/share/mon/itca.pem
new file mode 100644
index 0000000..5aaef20
--- /dev/null
+++ b/debian/mon/usr/share/mon/itca.pem
@@ -0,0 +1,23 @@
+-----BEGIN CERTIFICATE-----
+MIID0DCCArgCAQAwDQYJKoZIhvcNAQEEBQAwga0xGjAYBgNVBAcTEUQtNzYxMzUg
+S2FybHNydWhlMQ4wDAYDVQQDEwVJVC1DQTEbMBkGA1UEChMSU2NobHVuZCtQYXJ0
+bmVyIEFHMRIwEAYDVQQLEwlJVCBXRUIuREUxJDAiBgkqhkiG9w0BCQEWFWl0LW9w
+ZXJhdGluZ0B3ZWJkZS5kZTELMAkGA1UEBhMCREUxGzAZBgNVBAgTEkJhZGVuLVd1
+ZXJ0dGVtYmVyZzAeFw0wNjA5MDUxMTQzMDFaFw0xNjA5MDIxMTQzMDFaMIGtMRow
+GAYDVQQHExFELTc2MTM1IEthcmxzcnVoZTEOMAwGA1UEAxMFSVQtQ0ExGzAZBgNV
+BAoTElNjaGx1bmQrUGFydG5lciBBRzESMBAGA1UECxMJSVQgV0VCLkRFMSQwIgYJ
+KoZIhvcNAQkBFhVpdC1vcGVyYXRpbmdAd2ViZGUuZGUxCzAJBgNVBAYTAkRFMRsw
+GQYDVQQIExJCYWRlbi1XdWVydHRlbWJlcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IB
+DwAwggEKAoIBAQC9BUNmQbH7A6AYm5ABESPxROOSd5MrXPbo8V+Lfc0whZMc4x/I
+6TmMl1t5yk5SyOQ6YGm5k86Q6YUgz1kWd4xfiUMUgd6skiUmcpR1vs/UkTqK1T3t
+szwm3jLUFA9oFwi5SWLOlP4r1bwIJcP/EAgT5roLuIfruV9G6AnEconRnnnI0FcM
+hSyta0saLsSBFzfU24NVh/zbzs1DV03POxd/xvc222bnsWM++5a7gMbMHTVwb73Y
+JpqEVQLA8klU4Ko0GZPc6OFFgrAKl4rcj7JryK5iFbTKFRk45huPciBhkQ5XlVhr
+gKn3xD7ZQr40hMIdIbc5lLAELeifVQcsdbCBAgMBAAEwDQYJKoZIhvcNAQEEBQAD
+ggEBAJUC5MF121nZh/3/2YCgi7HedafMDg9sjKiBG9gWlpRVTw4hRSBlQq5V32sC
++GPL9VWD8IN+U3Vjiw5ja8j0mza2EomEdpL52EeWcGmQtSjOGnQ1MsXJiCZk+Q/P
+qGwgJpgwdubcnTmCH332sWTurgO53M4v61+VPj+N3MGMtaehzIo062lzXZyFdCS6
+SjTF1WdWbi/5nybNs8ffnG+p+uf37trJtopgndiVDi9k8WzS1HzWQf8Cnvy9X/U2
+5kZ4T6eVtrHujuP4PvG6PpfrZqEXENngsYHG0jtNTH06Ewtb7y/VDuqwYGsvSGm6
+RbB+lGFa1z1MzlWbBpPGckUgSHU=
+-----END CERTIFICATE-----
diff --git a/debian/mon/usr/share/mon/lib/MAPI/Cache.pm b/debian/mon/usr/share/mon/lib/MAPI/Cache.pm
new file mode 100644
index 0000000..21b59f5
--- /dev/null
+++ b/debian/mon/usr/share/mon/lib/MAPI/Cache.pm
@@ -0,0 +1,55 @@
+package MON::Cache;
+
+use strict;
+use warnings;
+use v5.10;
+use autodie;
+
+use Data::Dumper;
+
+use MON::Display;
+use MON::Config;
+use MON::Utils;
+
+our @ISA = ('MON::Display');
+
+sub new {
+ my ( $class, %opts ) = @_;
+
+ my $self = bless \%opts, $class;
+
+ $self->init();
+
+ return $self;
+}
+
+sub init {
+ my ($self) = @_;
+
+ $self->clear();
+
+ return undef;
+}
+
+sub clear {
+ my ($self) = @_;
+
+ $self->{cache} = {};
+
+ return undef;
+}
+
+sub magic {
+ my ( $self, $key, $sub ) = @_;
+
+ my $cache = $self->{cache};
+
+ if ( exists $cache->{$key} ) {
+ $self->verbose("Delivering '$key' from cache");
+ return $cache->{$key};
+ }
+
+ return $cache->{$key} = $sub->();
+}
+
+1;
diff --git a/debian/mon/usr/share/mon/lib/MAPI/Config.pm b/debian/mon/usr/share/mon/lib/MAPI/Config.pm
new file mode 100644
index 0000000..dc83911
--- /dev/null
+++ b/debian/mon/usr/share/mon/lib/MAPI/Config.pm
@@ -0,0 +1,176 @@
+package MON::Config;
+
+use strict;
+use warnings;
+use v5.10;
+use autodie;
+
+use IO::File;
+use Data::Dumper;
+
+use MON::Display;
+use MON::Utils;
+
+#use MON::Options;
+
+use MIME::Base64 qw( decode_base64 );
+
+our @ISA = ('MON::Display');
+
+sub new {
+ my ( $class, %opts ) = @_;
+
+ my $self = bless \%opts, $class;
+ my $options = $self->{options};
+
+ $options->store_first($self);
+
+ $self->SUPER::init(%opts);
+
+ for ( @{ $options->{unknown} } ) {
+ $self->error("Unknown option: $_");
+ }
+
+ if ( $self->{'config'} ne '' ) {
+ $self->read_config( $self->{'config'} );
+
+ }
+ elsif ( exists $ENV{MON_CONFIG} ) {
+ $self->read_config( $ENV{MON_CONFIG} );
+
+ }
+ else {
+ $self->read_config('/etc/mon.conf');
+ $self->read_config($_) for sort glob("/etc/mon.d/*.conf");
+
+ $self->read_config("$ENV{HOME}/.mon.conf");
+ $self->read_config($_) for sort glob("$ENV{HOME}/.mon.d/*.conf");
+ }
+
+ $options->store_after($self);
+
+ unless ( exists $self->{config_was_read} ) {
+ $self->verbose("No config file found, but this might be OK");
+ }
+
+ $self->_set_defaults();
+
+ return $self;
+}
+
+sub _set_defaults {
+ my ($self) = @_;
+
+ my $set_default = sub {
+ my ( $key, $val ) = @_;
+
+ unless ( exists $self->{$key} ) {
+ $self->{$key} = $val;
+ $self->verbose(
+ "Since $key is not specified setting its default value to $val");
+ }
+ };
+
+ $set_default->( 'backups.dir' => "$ENV{HOME}/.mon" );
+ $set_default->( 'backups.disable' => 1 );
+ $set_default->( 'backups.keep.days' => 7 );
+ $set_default->( 'restlos.api.port' => '443' );
+ $set_default->( 'restlos.api.protocol' => 'https' );
+ $set_default->( 'restlos.auth.realm' => 'Login Required' );
+ $set_default->( 'restlos.auth.username' => $ENV{USER} );
+}
+
+sub read_config {
+ my ( $self, $config_file ) = @_;
+
+ return undef if not defined $config_file or not -f $config_file;
+
+ my $fh = IO::File->new( $config_file, 'r' );
+ $self->error("Could not open file $config_file") unless defined $fh;
+
+ $self->verbose("Reading config $config_file");
+
+ while ( my $line = $fh->getline() ) {
+ next if $line =~ /^#/;
+
+ # Ignore comments
+ $line =~ s/(.*);.*/$1/;
+
+ # Parse only matching lines
+ if ( $line =~ /^(.*):(.*)/ ) {
+ my ( $key, $val ) = ( lc trim $1, trim $2);
+ $self->verbose("Reading conf value $key");
+
+ # Handle ~
+ $val =~ s/~/$ENV{HOME}/g;
+ $self->set( $key, $val );
+ }
+ }
+
+ $fh->close();
+ $self->{config_was_read} = 1;
+
+ return undef;
+}
+
+sub get {
+ my ( $self, $key ) = @_;
+ $key = lc $key;
+
+ $self->{$key} //= do {
+ my $key = uc $key;
+ $key =~ s/\./_/g;
+
+ exists $ENV{$key} ? $ENV{$key} : undef;
+ };
+
+ if ( not exists $self->{$key}
+ or not defined $self->{$key}
+ or $self->{$key} eq '' )
+ {
+ $self->error("$key not configured");
+ }
+
+ return $self->{$key};
+}
+
+sub get_maybe_encoded {
+ my ( $self, $key ) = @_;
+
+ return $self->get($key) if exists $self->{$key};
+
+ $self->error("$key or $key.enc not configured")
+ unless exists $self->{"$key.enc"};
+
+ my $enc = $self->get("$key.enc");
+
+ return decode_base64($enc);
+}
+
+sub bool {
+ my ( $self, $key ) = @_;
+
+ my $val = $self->get($key);
+
+ return $val != 0;
+}
+
+sub array {
+ my ( $self, $key ) = @_;
+
+ my $val = $self->get($key);
+
+ return map { trim $_ } split ',', $val;
+}
+
+sub set {
+ my ( $self, $key, $val ) = @_;
+ $key = lc $key;
+
+ $self->verbose("$key already configured, overwriting it with its new value")
+ if exists $self->{$key};
+
+ return $self->{$key} = $val;
+}
+
+1;
diff --git a/debian/mon/usr/share/mon/lib/MAPI/Display.pm b/debian/mon/usr/share/mon/lib/MAPI/Display.pm
new file mode 100644
index 0000000..9bf8115
--- /dev/null
+++ b/debian/mon/usr/share/mon/lib/MAPI/Display.pm
@@ -0,0 +1,360 @@
+package MON::Display;
+
+use strict;
+use warnings;
+use v5.10;
+use autodie;
+
+use Data::Dumper;
+use Term::ANSIColor;
+
+use MON::Config;
+use MON::JSON;
+use MON::Utils;
+
+our $VERBOSE = 0;
+our $DEBUG = 0;
+our $COLORFUL = 0;
+our $QUIET = 0;
+our $LOGGER = undef;
+our $INTERACTIVE = undef;
+
+sub init {
+ my ( $self, %opts ) = @_;
+
+ $VERBOSE = $self->{'verbose'} == 1;
+ $DEBUG = $self->{'debug'} == 1;
+ $QUIET = $self->{'quiet'} == 1;
+ $LOGGER = $opts{logger};
+ $INTERACTIVE = $opts{interactive};
+
+ $self->{logglevel} = 'info';
+
+ if ( $self->{'nocolor'} == 1 ) {
+ $COLORFUL = 0;
+ }
+ else {
+ $COLORFUL = $ENV{MON_COLORFUL} // 1;
+ }
+
+ $VERBOSE = $DEBUG = $COLORFUL = 0 if $QUIET == 1;
+
+ return undef;
+}
+
+sub is_verbose {
+ my ($self) = @_;
+
+ return $VERBOSE == 1;
+}
+
+sub is_debug {
+ my ($self) = @_;
+
+ return $DEBUG == 1;
+}
+
+sub is_quiet {
+ my ($self) = @_;
+
+ return $QUIET == 1;
+}
+
+sub _display {
+ my ( $self, $msg, $fh, $level ) = @_;
+
+ return undef unless defined $msg;
+
+ $LOGGER->logg( $self->{logglevel}, $msg ) if defined $LOGGER;
+
+ return undef if $QUIET;
+
+ $fh = *STDERR unless defined $fh;
+
+ print $fh $msg;
+
+ return undef;
+}
+
+sub info_no_nl {
+ my ( $self, $msg ) = @_;
+
+ print STDERR color 'bold blue' if $COLORFUL;
+ $self->_display($msg);
+ print STDERR color 'reset' if $COLORFUL;
+
+ return undef;
+}
+
+sub out_json {
+ my ( $self, $out ) = @_;
+
+ return undef unless defined $out;
+ my $config = $self->{config};
+
+ local $, = "\n";
+
+ my $json = MON::JSON->new()->decode($out);
+ my $num_results = ref $json eq 'ARRAY' ? @$json : undef;
+
+ # Don't _display meta aka custom variables unless -m or --meta is specified
+ unless ( $config->{'meta'} ) {
+ if ( ref $json eq 'ARRAY' ) {
+ @$json = map {
+ if ( ref $_ eq 'HASH' )
+ {
+ my $h = $_;
+ delete $h->{$_} for grep /^_/, keys %$h;
+ $h;
+ }
+ else {
+ $_;
+ }
+ } @$json;
+ }
+ }
+
+ # Sort and pretty print all the JSON pretty pretty please
+ unless ( defined $config->{outfile} ) {
+ print MON::JSON->new()->encode_canonical($json) unless $QUIET;
+ }
+ else {
+ my $outfile = $config->{outfile};
+ print $outfile MON::JSON->new()->encode_canonical($json);
+ print STDERR color 'bold green' if $COLORFUL;
+ $self->_display("Wrote JSON to file\n");
+ print STDERR color 'reset' if $COLORFUL;
+ }
+
+ $LOGGER->logg( 'info', JSON->new()->encode($json) ) if defined $LOGGER;
+
+ print STDERR color 'bold green' if $COLORFUL;
+ $self->_display("Found $num_results entries\n") if defined $num_results;
+ print STDERR color 'reset' if $COLORFUL;
+
+ return undef;
+}
+
+sub out_format {
+ my ( $self, $format, $out ) = @_;
+
+ return undef unless defined $out;
+
+ my $config = $self->{config};
+ my $options = $self->{options};
+ my $json = MON::JSON->new()->decode($out);
+ my $num_results = ref $json eq 'ARRAY' ? @$json : undef;
+
+ $self->error("Expected an JSON Array") if ref $json ne 'ARRAY';
+
+ my @vars1 = $format =~ /\$(\w+)/g;
+ my @vars2 = $format =~ /\$\{(\w+)\}/g;
+ my @vars3 = $format =~ /\@(\w+)/g;
+ my @vars4 = $format =~ /\@\{(\w+)\}/g;
+
+ my %vars;
+ $vars{$_} = '' for @vars1, @vars2, @vars3, @vars4;
+ my @out;
+ my %empty;
+
+ for my $obj (@$json) {
+ my %obj_vars = %vars;
+ my $obj_format = $format;
+
+ for my $var ( keys %obj_vars ) {
+ if ( $var eq 'HOSTNAME' ) {
+ my $val = exists $obj->{host_name} ? $obj->{host_name} : '';
+
+ if ( $val eq '' ) {
+ $empty{$var} = 1;
+ }
+ else {
+ $val =~ s/\..*//;
+ }
+
+ $obj_format =~ s/\$$var/$val/g;
+
+ }
+ else {
+ my $val = exists $obj->{$var} ? $obj->{$var} : '';
+ $empty{$var} = 1 if $val eq '';
+
+ $obj_format =~ s/\$$var/$val/g;
+ $obj_format =~ s/\$\{$var\}/$val/g;
+ $obj_format =~ s/\@$var/$val/g;
+ $obj_format =~ s/\@\{$var\}/$val/g;
+ }
+ }
+
+ push @out, $obj_format if $obj_format =~ /^.*\w+.*$/;
+ }
+
+ if (@out) {
+
+ if ( $config->{'unique'} ) {
+ my %lines;
+ @out = grep { exists $lines{$_} ? 0 : ( $lines{$_} = 1 ) } sort @out;
+ $num_results = @out;
+ }
+ else {
+ @out = sort @out;
+ }
+
+ if ( $QUIET == 0 ) {
+ local $, = "\n";
+ print @out;
+ say '';
+ }
+ elsif ( defined $LOGGER ) {
+ $LOGGER->logg( 'info', $_ ) for @out;
+ }
+ }
+
+ $self->warning( "Some objects dont have such a field or have empty strings: "
+ . join( ' ', sort keys %empty ) )
+ if keys %empty;
+
+ print STDERR color 'bold green' if $COLORFUL;
+ $self->_display("Found $num_results entries\n") if defined $num_results;
+ print STDERR color 'reset' if $COLORFUL;
+
+ return undef;
+}
+
+sub info {
+ my ( $self, $msg ) = @_;
+
+ my $str = "$msg\n";
+ $self->{logglevel} = 'info';
+
+ print STDERR color 'bold blue' if $COLORFUL;
+ $self->_display($str);
+ print STDERR color 'reset' if $COLORFUL;
+
+ return undef;
+}
+
+sub nl {
+ my ($self) = @_;
+
+ $self->_display("\n");
+
+ return undef;
+}
+
+sub error {
+ my ( $self, $msg ) = @_;
+
+ $self->error_no_exit($msg);
+
+ exit 3 unless $INTERACTIVE;
+}
+
+sub error_no_exit {
+ my ( $self, $msg ) = @_;
+
+ $self->{logglevel} = 'warning';
+ print STDERR color 'bold red' if $COLORFUL;
+ $self->_display( "! ERROR: $msg\n", *STDERR );
+ print STDERR color 'reset' if $COLORFUL;
+
+ return undef;
+}
+
+sub possible {
+ my ( $self, @params ) = @_;
+
+ my $config = $self->{config};
+ my $options = $self->{options};
+
+ push @params, $options->get_keys()
+ if $config->{'help'};
+
+ my $msg = '';
+
+ if (@params) {
+ for ( grep !/^V_ALIAS/, @params ) {
+ if ( ref $_ eq 'ARRAY' ) {
+ $msg .= join "\n", @$_;
+ $msg .= "\n";
+ }
+ else {
+ $msg .= "$_\n";
+ }
+ }
+ }
+ else {
+ $msg .= "\n";
+ }
+
+ $self->{logglevel} = 'info';
+ $self->_display($msg);
+
+ exit 0 unless $INTERACTIVE;
+}
+
+sub warning {
+ my ( $self, $msg ) = @_;
+
+ my $str = "! $msg\n";
+
+ print STDERR color 'red' if $COLORFUL;
+ $self->_display( $str, *STDERR );
+ print STDERR color 'reset' if $COLORFUL;
+
+ return undef;
+}
+
+sub verbose {
+ my ( $self, @msgs ) = @_;
+
+ print STDERR color 'cyan' if $COLORFUL;
+ $self->{logglevel} = 'info';
+
+ if ( $self->is_verbose() ) {
+ for my $msg (@msgs) {
+ if ( $self->is_debug() ) {
+ my @caller = caller;
+ $self->_display("@caller: $msg\n");
+ }
+ else {
+ $self->_display("$msg\n");
+ }
+ }
+ }
+
+ print STDERR color 'reset' if $COLORFUL;
+
+ return undef;
+}
+
+sub dump {
+ my ( $self, $msg ) = @_;
+
+ $self->{logglevel} = 'warning';
+ $self->_display( Dumper $msg );
+
+ return undef;
+}
+
+sub debug {
+ my ( $self, @msgs ) = @_;
+
+ my @caller = caller;
+
+ if ( $self->is_debug() ) {
+ for my $msg (@msgs) {
+ $msg = Dumper $msg if ref $msg ne '';
+
+ my $str = "@caller: $msg\n";
+
+ $self->{logglevel} = 'debug';
+ $self->_display($str);
+ }
+ }
+
+ return undef;
+}
+
+1;
+
diff --git a/debian/mon/usr/share/mon/lib/MAPI/Filter.pm b/debian/mon/usr/share/mon/lib/MAPI/Filter.pm
new file mode 100644
index 0000000..d16d1c5
--- /dev/null
+++ b/debian/mon/usr/share/mon/lib/MAPI/Filter.pm
@@ -0,0 +1,166 @@
+package MON::Filter;
+
+use strict;
+use warnings;
+use v5.10;
+use autodie;
+
+use Data::Dumper;
+
+use MON::Display;
+use MON::Config;
+use MON::Utils;
+
+our @ISA = ('MON::Display');
+
+sub new {
+ my ( $class, %opts ) = @_;
+
+ my $self = bless \%opts, $class;
+
+ $self->init();
+
+ return $self;
+}
+
+sub init {
+ my ($self) = @_;
+
+ $self->{query_string} = '';
+ $self->{filters} = {};
+ $self->{num_filters} = 0;
+ $self->{is_computed} = 0;
+ $self->{or} = [];
+
+ return undef;
+}
+
+# Create filters with params
+sub compute {
+ my ( $self, $params ) = @_;
+
+ $self->debug( 'Computing filter using', $params );
+ return undef if $self->{is_computed};
+
+ my %likes;
+
+ if ( defined $params and ref $params eq 'ARRAY' ) {
+ while (@$params) {
+ my $op_token = pop @$params;
+ given ($op_token) {
+ when (/^OP_LIKE$/) {
+ my $arg2 = pop @$params;
+ my $arg1 = pop @$params;
+
+ if ( exists $likes{$arg1} ) {
+ $self->error(
+"Can not run multiple 'like's on '$arg1', since it is used for the API query_string"
+ );
+ }
+ else {
+ $likes{$arg1} = "$arg1=$arg2";
+ }
+
+ }
+ when (/^OP_/) {
+ $self->{filters}{$_} = [] unless exists $self->{filters}{$_};
+ my $arg2 = pop @$params;
+ my $arg1 = pop @$params;
+ push @{ $self->{filters}{$_} }, [ $arg1, $arg2 ];
+ $self->{num_filters}++;
+ }
+ default {
+ $self->error("Inernal error: Operator expected instead of $_");
+ }
+ }
+ }
+ }
+
+ $self->{query_string} = '?' . join( '&', values %likes );
+ $self->{is_computed} = 1;
+
+ $self->debug( 'Computed filter:', $self->{filters} );
+ $self->verbose( "Computed query string is: " . $self->{query_string} );
+
+ return undef;
+}
+
+sub filter {
+ my ( $self, $objects ) = @_;
+
+ my $config = $self->{config};
+ my $json = $self->{json};
+
+ return $objects unless $self->{num_filters};
+
+ my $num = sub {
+ my $str = shift;
+ $str =~ s/\D//g;
+ $str = 0 if $str eq '';
+ return int $str;
+ };
+
+ while ( my ( $op, $vals ) = each %{ $self->{filters} } ) {
+ for my $val (@$vals) {
+ my ( $key, $val ) = @$val;
+
+ @$objects = grep {
+ my $object = $_;
+
+ if ( exists $object->{$key} ) {
+ if ( $op eq 'OP_MATCHES' and $object->{$key} =~ /$val/ ) {
+ 1;
+
+ }
+ elsif ( $op eq 'OP_NMATCHES' and $object->{$key} !~ /$val/ ) {
+ 1;
+
+ }
+ elsif ( $op eq 'OP_EQ' and $object->{$key} eq $val ) {
+ 1;
+
+ }
+ elsif ( $op eq 'OP_NE' and $object->{$key} ne $val ) {
+ 1;
+
+ }
+ elsif ( $op eq 'OP_LT'
+ and $num->( $object->{$key} ) < $num->($val) )
+ {
+ 1;
+
+ }
+ elsif ( $op eq 'OP_LE'
+ and $num->( $object->{$key} ) <= $num->($val) )
+ {
+ 1;
+
+ }
+ elsif ( $op eq 'OP_GT'
+ and $num->( $object->{$key} ) > $num->($val) )
+ {
+ 1;
+
+ }
+ elsif ( $op eq 'OP_GE'
+ and $num->( $object->{$key} ) >= $num->($val) )
+ {
+ 1;
+
+ }
+ else {
+ 0;
+ }
+ }
+ else {
+ 0;
+ }
+ } @$objects;
+ }
+ }
+
+ return $objects;
+}
+
+1;
+
diff --git a/debian/mon/usr/share/mon/lib/MAPI/JSON.pm b/debian/mon/usr/share/mon/lib/MAPI/JSON.pm
new file mode 100644
index 0000000..e12b1ce
--- /dev/null
+++ b/debian/mon/usr/share/mon/lib/MAPI/JSON.pm
@@ -0,0 +1,51 @@
+package MON::JSON;
+
+use strict;
+use warnings;
+use v5.10;
+use autodie;
+
+use JSON;
+
+use MON::Display;
+use MON::Utils;
+
+our @ISA = ('MON::Display');
+
+our $JSON_XS = JSON::XS->new();
+
+sub new {
+ my ( $class, %opts ) = @_;
+
+ my $self = bless \%opts, $class;
+
+ $self->init();
+
+ return $self;
+}
+
+sub init {
+ my ($self) = @_;
+
+ return undef;
+}
+
+sub decode {
+ my ( $self, $json ) = @_;
+
+ return $JSON_XS->allow_nonref()->decode($json);
+}
+
+sub encode {
+ my ( $self, $vals ) = @_;
+
+ return $JSON_XS->pretty()->encode($vals);
+}
+
+sub encode_canonical {
+ my ( $self, $vals ) = @_;
+
+ return $JSON_XS->canonical()->pretty()->encode($vals);
+}
+
+1;
diff --git a/debian/mon/usr/share/mon/lib/MAPI/Options.pm b/debian/mon/usr/share/mon/lib/MAPI/Options.pm
new file mode 100644
index 0000000..b798f56
--- /dev/null
+++ b/debian/mon/usr/share/mon/lib/MAPI/Options.pm
@@ -0,0 +1,163 @@
+package MON::Options;
+
+use strict;
+use warnings;
+use v5.10;
+use autodie;
+
+use Data::Dumper;
+use Scalar::Util qw(looks_like_number);
+
+use MON::Utils;
+
+sub new {
+ my ( $class, %opts ) = @_;
+
+ my $self = bless \%opts, $class;
+
+ $self->init();
+ $self->parse();
+
+ return $self;
+}
+
+sub init {
+ my ($self) = @_;
+
+ my %opts = (
+ opts => {
+ config => '',
+ debug => 0,
+ dry => 0,
+ help => 0,
+ interactive => 0,
+ meta => 0,
+ nocolor => 0,
+ quiet => 0,
+ syslog => 0,
+ unique => 0,
+ verbose => 0,
+ version => 0,
+ errfile => '',
+ },
+ opts_short => {
+ c => 'config',
+ D => 'debug',
+ d => 'dry',
+ i => 'interactive',
+ h => 'help',
+ m => 'meta',
+ n => 'nocolor',
+ q => 'quiet',
+ s => 'syslog',
+ u => 'unique',
+ v => 'verbose',
+ V => 'version',
+ R => 'errfile',
+ },
+ unknown => [],
+ );
+
+ $self->{$_} = $opts{$_} for keys %opts;
+
+ return undef;
+}
+
+sub parse {
+ my ($self) = @_;
+
+ my $opts_passed = $self->{opts_passed};
+
+ for my $opt (@$opts_passed) {
+ my ( $k, $v ) = split /=/, $opt;
+
+ # Longopt
+ if ( $k =~ s/^--// && isin $k, keys %{ $self->{opts} } ) {
+ if ( defined $v ) {
+ $self->{opts}{$k} = $v;
+ }
+ else {
+ $self->{opts}{$k} = 1;
+ }
+ }
+
+ # Shortopt
+ elsif ( $k =~ s/^-// && isin $k, keys %{ $self->{opts_short} } ) {
+ if ( defined $v ) {
+ $self->{opts}{ $self->{opts_short}{$k} } = $v;
+ }
+ else {
+ $self->{opts}{ $self->{opts_short}{$k} } = 1;
+ }
+
+ }
+ elsif ( $k !~ /\./ ) {
+
+ # If key is not separated by dot, it is unknown
+ push @{ $self->{unknown} }, $opt;
+
+ }
+ else {
+
+ # Otherise it might overwrite a value of mon.conf
+ $self->{opts}{$k} = $v;
+ }
+ }
+
+ # Help implies dry mode
+ $self->{opts}{dry} = 1 if $self->{opts}{help};
+
+ # Debug implies verbose mode
+ $self->{opts}{verbose} = 1 if $self->{opts}{debug};
+
+ return undef;
+}
+
+sub get_keys {
+ my ($self) = @_;
+ my @keys;
+
+ while ( my ( $k, $v ) = each %{ $self->{opts_short} } ) {
+ if ( looks_like_number( $self->{opts}{$v} ) ) {
+ push @keys, "--$v -$k";
+ }
+ else {
+ push @keys, "--$v=VAL -$k=VAL";
+ }
+ }
+
+ return @keys;
+}
+
+sub store {
+ my ( $self, $config ) = @_;
+
+ $self->store_first($config);
+ $self->store_after($config);
+
+ return undef;
+}
+
+# Only store values which are not separated by dots
+sub store_first {
+ my ( $self, $config ) = @_;
+
+ for ( grep !/\./, keys %{ $self->{opts} } ) {
+ $config->{$_} = $self->{opts}{$_};
+ }
+
+ return undef;
+}
+
+# Only store values which are separated by dots
+sub store_after {
+ my ( $self, $config ) = @_;
+
+ for ( grep /\./, keys %{ $self->{opts} } ) {
+ $config->{$_} = $self->{opts}{$_};
+ }
+
+ return undef;
+}
+
+1;
diff --git a/debian/mon/usr/share/mon/lib/MAPI/Query.pm b/debian/mon/usr/share/mon/lib/MAPI/Query.pm
new file mode 100644
index 0000000..d7e223b
--- /dev/null
+++ b/debian/mon/usr/share/mon/lib/MAPI/Query.pm
@@ -0,0 +1,557 @@
+package MON::Query;
+
+use strict;
+use warnings;
+use v5.10;
+
+use Data::Dumper;
+
+use MON::Display;
+use MON::Config;
+use MON::Utils;
+use MON::QueryBase;
+
+our @ISA = ('MON::QueryBase');
+
+sub new {
+ my ( $class, %opts ) = @_;
+
+ my $self = bless \%opts, $class;
+
+ $self->init();
+
+ return $self;
+}
+
+sub init {
+ my ($self) = @_;
+
+ $self->{querystack} = [];
+ $self->{args} = [ map { s/^V_/:V_/; $_ } @{ $self->{args} } ];
+
+ return undef;
+}
+
+sub tree {
+ my ($self) = @_;
+
+ my $api = $self->{api};
+ my $paths = $api->get_possible_paths();
+
+ my ( $s, $r ) = ( $self, $api );
+
+ my $arr = sub {
+ my ( $keys, $vals ) = @_;
+ map { $_ => shift @$vals } @$keys;
+ };
+
+# _ => By default to run anonymous sub if no other key is specified in command line
+# __ => Always to run anonymous sub in the beginning of the current recursion
+# ___ => Always to run anonymous sub before next recursion
+# __DO => Process recursion right away, only do __ if exists
+# V_FOO => Declare variable FOO
+
+ my $where = {
+ _ => sub {
+ my $d = shift;
+ $s->possible( $r->get_path_params( $d->{V_PATH} ) );
+ },
+ V_KEY => {
+ __ => sub {
+ my $d = shift;
+ $s->check_has( $d->{V_KEY}, $r->get_path_params( $d->{V_PATH} ) );
+ },
+ like => {
+ V_VAL => {
+ _ => sub {
+ my $d = shift;
+ my $path = $d->{V_PATH};
+ $s->out_json( $d->{where_action}($path) );
+ },
+ },
+ ___ => sub { $s->push_querystack('OP_LIKE') }
+ },
+ matches => {
+ V_VAL => {
+ _ => sub {
+ my $d = shift;
+ my $path = $d->{V_PATH};
+ $s->out_json( $d->{where_action}($path) );
+ },
+ },
+ ___ => sub { $s->push_querystack('OP_MATCHES') }
+ },
+ nmatches => {
+ V_VAL => {
+ _ => sub {
+ my $d = shift;
+ my $path = $d->{V_PATH};
+ $s->out_json( $d->{where_action}($path) );
+ },
+ },
+ ___ => sub { $s->push_querystack('OP_NMATCHES') }
+ },
+ eq => {
+ V_VAL => {
+ _ => sub {
+ my $d = shift;
+ my $path = $d->{V_PATH};
+ $s->out_json( $d->{where_action}($path) );
+ },
+ },
+ ___ => sub { $s->push_querystack('OP_EQ') }
+ },
+ ne => {
+ V_VAL => {
+ _ => sub {
+ my $d = shift;
+ my $path = $d->{V_PATH};
+ $s->out_json( $d->{where_action}($path) );
+ },
+ },
+ ___ => sub { $s->push_querystack('OP_NE') }
+ },
+ lt => {
+ V_VAL => {
+ _ => sub {
+ my $d = shift;
+ my $path = $d->{V_PATH};
+ $s->out_json( $d->{where_action}($path) );
+ },
+ },
+ ___ => sub { $s->push_querystack('OP_LT') }
+ },
+ le => {
+ V_VAL => {
+ _ => sub {
+ my $d = shift;
+ my $path = $d->{V_PATH};
+ $s->out_json( $d->{where_action}($path) );
+ },
+ },
+ ___ => sub { $s->push_querystack('OP_LE') }
+ },
+ gt => {
+ V_VAL => {
+ _ => sub {
+ my $d = shift;
+ my $path = $d->{V_PATH};
+ $s->out_json( $d->{where_action}($path) );
+ },
+ },
+ ___ => sub { $s->push_querystack('OP_GT') }
+ },
+ ge => {
+ V_VAL => {
+ _ => sub {
+ my $d = shift;
+ my $path = $d->{V_PATH};
+ $s->out_json( $d->{where_action}($path) );
+ },
+ },
+ ___ => sub { $s->push_querystack('OP_GE') }
+ },
+ },
+ };
+
+ for my $op ( sort qw(like matches nmatches eq ne lt le gt ge) ) {
+ $where->{V_KEY}{$op}{V_VAL}{and}{__DO} = $where;
+ $where->{V_KEY}{$op}{V_VAL}{'V_ALIAS:a'} = $where->{V_KEY}{$op}{V_VAL}{and};
+ }
+
+ $where->{V_KEY}{'V_ALIAS:l'} = $where->{V_KEY}{like};
+ $where->{V_KEY}{'V_ALIAS:~'} = $where->{V_KEY}{like};
+ $where->{V_KEY}{'V_ALIAS:=='} = $where->{V_KEY}{eq};
+ $where->{V_KEY}{'V_ALIAS:!='} = $where->{V_KEY}{ne};
+ $where->{V_KEY}{'V_ALIAS:=~'} = $where->{V_KEY}{matches};
+ $where->{V_KEY}{'V_ALIAS:!~'} = $where->{V_KEY}{nmatches};
+
+ my $set_where = {
+ _ => sub {
+ my $d = shift;
+ $s->possible( $r->get_path_params( $d->{V_PATH} ) );
+ },
+ V_SETKEY => {
+ '=' => {
+ V_SETVAL => {
+ __ => sub {
+ my $d = shift;
+ $d->{where_action} = $d->{set_action};
+ },
+ where => $where,
+ },
+ },
+ },
+ };
+ $set_where->{V_SETKEY}{'='}{V_SETVAL}{and} = $set_where;
+ $set_where->{V_SETKEY}{'='}{V_SETVAL}{'V_ALIAS::'} =
+ $set_where->{V_SETKEY}{'='}{V_SETVAL}{where};
+ $set_where->{V_SETKEY}{'='}{V_SETVAL}{'V_ALIAS:a'} =
+ $set_where->{V_SETKEY}{'='}{V_SETVAL}{and};
+
+ my $set = {
+ _ => sub {
+ my $d = shift;
+ $s->possible( $r->get_path_params( $d->{V_PATH} ) );
+ },
+ V_SETKEY => {
+ '=' => {
+ V_SETVAL => {
+ _ => sub {
+ my $d = shift;
+ my $path = $d->{V_PATH};
+
+ $s->out_json( $d->{set_action}($path) );
+ },
+ },
+ },
+ },
+ };
+ $set->{V_SETKEY}{'='}{V_SETVAL}{and} = $set;
+ $set->{V_SETKEY}{'='}{V_SETVAL}{'V_ALIAS:a'} =
+ $set->{V_SETKEY}{'='}{V_SETVAL}{and};
+
+ my $remove = {
+ _ => sub {
+ my $d = shift;
+ $s->possible( $r->get_path_params( $d->{V_PATH} ) );
+ },
+ V_REMOVEKEY => {
+ __ => sub {
+ my $d = shift;
+ $d->{where_action} = $d->{remove_action};
+ },
+ where => $where,
+ },
+ };
+ $remove->{V_REMOVEKEY}{and} = $remove;
+ $remove->{V_REMOVEKEY}{'V_ALIAS:a'} = $remove->{V_REMOVEKEY}{and};
+
+ my $tree = {
+ get => {
+ _ => sub { $s->possible(@$paths) },
+ V_PATH => {
+ __ => sub {
+ my $d = shift;
+ $s->check_has( $d->{V_PATH}, $paths );
+ $d->{where_action} = sub {
+ my ($path) = @_;
+ $r->fetch_path_json( $path, $s->get_querystack() );
+ };
+ },
+ _ => sub {
+ my $d = shift;
+ $s->out_json(
+ $r->fetch_path_json( $d->{V_PATH}, $s->get_querystack() ) );
+ },
+ where => $where,
+ },
+ },
+ getfmt => {
+ V_FORMAT => {
+ _ => sub { $s->possible(@$paths) },
+ V_PATH => {
+ __ => sub {
+ my $d = shift;
+ $s->check_has( $d->{V_PATH}, $paths );
+ $d->{where_action} = sub {
+ my ($path) = @_;
+ $s->out_format( $d->{V_FORMAT},
+ $r->fetch_path_json( $path, $s->get_querystack() ) );
+ };
+ },
+ _ => sub {
+ my $d = shift;
+ $s->out_format( $d->{V_FORMAT},
+ $r->fetch_path_json( $d->{V_PATH}, $s->get_querystack() ) );
+ },
+ where => $where,
+ },
+ },
+ },
+ edit => {
+ _ => sub { $s->possible(@$paths) },
+ V_PATH => {
+ __ => sub {
+ my $d = shift;
+ $s->check_has( $d->{V_PATH}, $paths );
+ $d->{where_action} = sub {
+ my ($path) = @_;
+ $s->edit_path_data( $path,
+ $r->fetch_path_json( $path, $s->get_querystack() ) );
+ };
+ },
+ _ => sub {
+ my $d = shift;
+ $s->edit_path_data( $d->{V_PATH},
+ $r->fetch_path_json( $d->{V_PATH} ) );
+ },
+ where => $where,
+ },
+ },
+ view => {
+ _ => sub { $s->possible(@$paths) },
+ V_PATH => {
+ __ => sub {
+ my $d = shift;
+ $s->check_has( $d->{V_PATH}, $paths );
+ $d->{where_action} = sub {
+ my ($path) = @_;
+ $s->view_data( $path,
+ $r->fetch_path_json( $path, $s->get_querystack() ) );
+ };
+ },
+ _ => sub {
+ my $d = shift;
+ $s->view_data( $d->{V_PATH}, $r->fetch_path_json( $d->{V_PATH} ) );
+ },
+ where => $where,
+ },
+ },
+ delete => {
+ _ => sub { $s->possible(@$paths) },
+ V_PATH => {
+ __ => sub {
+ my $d = shift;
+ $s->check_has( $d->{V_PATH}, $paths );
+ $d->{where_action} = sub {
+ my ($path) = @_;
+ $s->out_json( $r->delete_path_json( $path, $s->get_querystack() ) );
+ };
+ },
+ _ => sub {
+ my $d = shift;
+ $s->out_json( $r->fetch_path_json( $d->{V_PATH} ) );
+ },
+ where => $where,
+ },
+ },
+ update => {
+ _ => sub { $s->possible(@$paths) },
+ V_PATH => {
+ __ => sub {
+ my $d = shift;
+ $s->check_has( $d->{V_PATH}, $paths );
+ $d->{set_action} = sub {
+ my ($path) = @_;
+ my %set = $arr->( $d->{ALL_V_SETKEY}, $d->{ALL_V_SETVAL} );
+ $s->out_json(
+ $r->update_path_json( $path, $s->get_querystack(), \%set ) );
+ };
+ $d->{remove_action} = sub {
+ my ($path) = @_;
+ my $remove = $d->{ALL_V_REMOVEKEY};
+ $s->out_json(
+ $r->update_remove_path_json(
+ $path, $s->get_querystack(), $remove
+ )
+ );
+ };
+ },
+ set => $set_where,
+ 'delete' => $remove,
+ },
+ },
+ insert => {
+ _ => sub { $s->possible(@$paths) },
+ V_PATH => {
+ __ => sub {
+ my $d = shift;
+ $s->check_has( $d->{V_PATH}, $paths );
+ $d->{set_action} = sub {
+ my ($path) = @_;
+ my %set = $arr->( $d->{ALL_V_SETKEY}, $d->{ALL_V_SETVAL} );
+ $s->out_json( $self->insert_data( $path, \%set ) );
+ };
+ },
+ set => $set,
+ },
+ },
+ post => {
+ _ => sub { $s->possible(@$paths) },
+ V_PATH => {
+ __ => sub {
+ my $d = shift;
+ $s->check_has( $d->{V_PATH}, $paths );
+ },
+ _ => sub {
+ my $d = shift;
+ $s->send_data( $d->{V_PATH}, 'POST' );
+ },
+ from => {
+ V_FILEPATH => {
+ _ => sub {
+ my $d = shift;
+ $s->send_data( $d->{V_PATH}, 'POST', $d->{V_FILEPATH} );
+ }
+ }
+ }
+ },
+ },
+ put => {
+ _ => sub { $s->possible(@$paths) },
+ V_PATH => {
+ __ => sub {
+ my $d = shift;
+ $s->check_has( $d->{V_PATH}, $paths );
+ },
+ _ => sub {
+ my $d = shift;
+ $s->send_data( $d->{V_PATH}, 'PUT' );
+ },
+ from => {
+ V_FILEPATH => {
+ _ => sub {
+ my $d = shift;
+ $s->send_data( $d->{V_PATH}, 'PUT', $d->{V_FILEPATH} );
+ }
+ }
+ }
+ },
+ },
+ verify => sub { $s->verify() },
+ restart => sub { $s->restart() },
+ reload => sub { $s->restart() },
+ 'V_ALIAS:y' => sub { $s->verify() },
+ 'V_ALIAS:r' => sub { $s->restart() },
+ };
+
+ $tree->{delete}{V_PATH}{'V_ALIAS::'} = $tree->{delete}{V_PATH}{where};
+ $tree->{'V_ALIAS:d'} = $tree->{delete};
+
+ $tree->{edit}{V_PATH}{'V_ALIAS::'} = $tree->{edit}{V_PATH}{where};
+ $tree->{'V_ALIAS:e'} = $tree->{edit};
+
+ $tree->{get}{V_PATH}{'V_ALIAS::'} = $tree->{get}{V_PATH}{where};
+ $tree->{'V_ALIAS:g'} = $tree->{get};
+
+ $tree->{getfmt}{V_FORMAT}{V_PATH}{'V_ALIAS::'} =
+ $tree->{getfmt}{V_FORMAT}{V_PATH}{where};
+ $tree->{'V_ALIAS:f'} = $tree->{getfmt};
+
+ $tree->{insert}{V_PATH}{'V_ALIAS:s'} = $tree->{insert}{V_PATH}{set};
+ $tree->{'V_ALIAS:i'} = $tree->{insert};
+
+ $tree->{'V_ALIAS:p'} = $tree->{post};
+
+ $tree->{'V_ALIAS:t'} = $tree->{put};
+
+ $tree->{update}{V_PATH}{'V_ALIAS:d'} = $tree->{update}{V_PATH}{delete};
+ $tree->{update}{V_PATH}{'V_ALIAS:s'} = $tree->{update}{V_PATH}{set};
+ $tree->{'V_ALIAS:u'} = $tree->{update};
+
+ $tree->{view}{V_PATH}{'V_ALIAS::'} = $tree->{view}{V_PATH}{where};
+ $tree->{'V_ALIAS:v'} = $tree->{view};
+
+ $self->debug( 'Abstract syntax tree:', $tree );
+
+ return $tree;
+}
+
+sub parse {
+ my ($self) = @_;
+
+ my $config = $self->{config};
+ my $args = $self->{args};
+
+ # Get > and < operators (only needed by interactive mode)
+ $config->{outfile} = $config->{infile} = undef;
+ if ( defined $args->[-2] ) {
+ given ( $args->[-2] ) {
+ when ('>') {
+ my ( undef, $file ) = splice @$args, -2, 2;
+ open $config->{outfile}, '>', $file or $self->warning("$file: $!");
+ }
+ when ('<') {
+ my ( undef, $file ) = splice @$args, -2, 2;
+ open $config->{infile}, '<', $file or $self->warning("$file: $!");
+ }
+ }
+ }
+
+ my $ret = $self->traverse( $args, $self->tree(), {} );
+
+ close $config->{infile} if defined $config->{infile};
+ close $config->{outfile} if defined $config->{outfile};
+
+ return $ret;
+}
+
+sub traverse {
+ my ( $self, $args, $tree, $data ) = @_;
+
+ $self->debug( 'Traversing args: ' . Dumper $args);
+ $self->debug( 'Traversing data: ' . Dumper $data);
+
+ if ( ref $tree eq 'CODE' ) {
+ $tree->($data);
+ return undef;
+ }
+
+ $tree->{__}->($data) if exists $tree->{__};
+
+ if ( exists $tree->{__DO} ) {
+ $self->traverse( $args, $tree->{__DO}, $data );
+ return undef;
+ }
+
+ my @possible = grep !/^__?$/, sort keys %$tree;
+ my $token = $possible[0];
+
+ unless (@$args) {
+ if ( exists $tree->{_} ) {
+ $tree->{_}->($data);
+ }
+ else {
+ $self->possible(@possible);
+ }
+ }
+ else {
+ my $arg = shift @$args;
+
+ if ( exists $tree->{$arg} ) {
+ $tree->{___}->($data) if exists $tree->{___};
+ $self->traverse( $args, $tree->{$arg}, $data );
+ }
+ elsif ( exists $tree->{"V_ALIAS:$arg"} ) {
+ $tree->{___}->($data) if exists $tree->{___};
+ $self->traverse( $args, $tree->{"V_ALIAS:$arg"}, $data );
+ }
+ elsif ( defined $token && $token =~ /^V_/ && $token !~ /^V_ALIAS:/ ) {
+ $data->{$token} = $arg;
+ $self->push_querystack($arg) if $token =~ /^V_(?:KEY|VAL)/;
+
+ unless ( exists $data->{"ALL_$token"} ) {
+ $data->{"ALL_$token"} = [$arg];
+ }
+ else {
+ push @{ $data->{"ALL_$token"} }, $arg;
+ }
+
+ $tree->{___}->($data) if exists $tree->{___};
+ $self->traverse( $args, $tree->{$token}, $data );
+ }
+ else {
+ $self->error("'$arg' unexpected here");
+ }
+ }
+
+ return undef;
+}
+
+sub push_querystack {
+ my ( $self, $token ) = @_;
+
+ $self->debug("Pushing token '$token' to querystack");
+ push @{ $self->{querystack} }, $token;
+
+ return undef;
+}
+
+sub get_querystack {
+ my ($self) = @_;
+
+ return $self->{querystack};
+}
+
+1;
diff --git a/debian/mon/usr/share/mon/lib/MAPI/QueryBase.pm b/debian/mon/usr/share/mon/lib/MAPI/QueryBase.pm
new file mode 100644
index 0000000..6ddfb99
--- /dev/null
+++ b/debian/mon/usr/share/mon/lib/MAPI/QueryBase.pm
@@ -0,0 +1,232 @@
+package MON::QueryBase;
+
+use strict;
+use warnings;
+use v5.10;
+
+use File::Temp qw/:mktemp/;
+use Data::Dumper;
+use Digest::SHA;
+
+use MON::Display;
+use MON::Config;
+use MON::Utils;
+
+our @ISA = ('MON::Display');
+
+sub check_has {
+ my ( $self, $key, $in ) = @_;
+
+ if ( ref $in eq 'HASH' && exists $in->{$key} ) {
+ return 1;
+
+ }
+ else {
+ for (@$in) {
+ return 1 if $_ eq $key;
+ }
+ }
+
+ my @possible = sort ( ref $in eq 'HASH' ? keys %$in : @$in );
+ $self->error("'$key' not expected here. Possible: @possible");
+}
+
+sub edit_path_file_send {
+ my ( $self, $path, $filename ) = @_;
+
+ my $api = $self->{api};
+
+ open my $fh, $filename or die "$filename: $!";
+ my @data = <$fh>;
+ close $fh;
+
+ $self->info("Saving data to API into $path from file $filename");
+ $self->out_json(
+ $api->send_path_json( $path, join( '', @data ), undef, 'PUT' ) );
+
+ return undef;
+}
+
+sub get_sha_of_file {
+ my ( $self, $filename ) = @_;
+
+ my $sha = Digest::SHA->new();
+ open my $sha_fh, $filename or die "$!\n";
+ $sha->addfile($sha_fh);
+ $sha = $sha->b64digest();
+ close $sha_fh;
+
+ return $sha;
+}
+
+sub edit_path_file {
+ my ( $self, $path, $filename ) = @_;
+
+ my $config = $self->{config};
+ my $api = $self->{api};
+
+ if ( $config->{'dry'} ) {
+ $self->verbose("Dry mode, don't modify anything via API.");
+ return undef;
+ }
+
+ my $editor = $ENV{EDITOR} // 'vim';
+
+ my $sha_before = $self->get_sha_of_file($filename);
+ $self->verbose("Checksum of $filename before edit: $sha_before");
+
+ for ( ; ; ) {
+ system("$editor $filename");
+ my $sha_after = $self->get_sha_of_file($filename);
+ $self->verbose("Checksum of $filename after edit: $sha_after");
+
+ if ( $sha_before eq $sha_after ) {
+ $self->info(
+ "Dude, no changes were made. I am not sending data back to the API!");
+ last;
+ }
+
+ $self->edit_path_file_send( $path, $filename );
+ if ( $api->{has_error} ) {
+ $self->info('An error has occured, press any key to re-edit');
+ <STDIN>;
+ }
+ else {
+ last;
+ }
+ }
+
+ for ( glob("/tmp/mon*.json") ) {
+ $self->verbose("Cleaning up tempfile $_");
+ unlink $_;
+ }
+
+ return undef;
+}
+
+sub edit_path_data {
+ my ( $self, $path, $data ) = @_;
+
+ my $config = $self->{config};
+ my $api = $self->{api};
+ my $json = $api->{json};
+
+ my ( $fh, $filename ) = mkstemps( "/tmp/monXXXXXX", '.json' );
+
+ # Sort the json
+ my $vals = $json->decode($data);
+ print $fh $json->encode_canonical($vals);
+ close $fh;
+
+ $self->edit_path_file( $path, $filename );
+
+ return undef;
+}
+
+sub view_data {
+ my ( $self, $path, $data ) = @_;
+
+ my $config = $self->{config};
+ my $api = $self->{api};
+ my $json = $api->{json};
+
+ if ( $config->{'dry'} ) {
+ $self->verbose("Dry mode, don't modify anything via API.");
+ return undef;
+ }
+ my ( $fh, $filename ) = mkstemps( "/tmp/monXXXXXX", '.json' );
+
+ # Sort the json
+ my $vals = $json->decode($data);
+ print $fh $json->encode_canonical($vals);
+ close $fh;
+
+ my $editor = $ENV{PAGER} // 'view';
+ system("$editor $filename");
+
+ unlink $filename;
+ return undef;
+}
+
+sub insert_data {
+ my ( $self, $path, $set ) = @_;
+
+ my $config = $self->{config};
+ my $api = $self->{api};
+
+ if ( $config->{'dry'} ) {
+ $self->verbose("Dry mode, don't modify anything via API.");
+ return undef;
+ }
+
+ return $api->send_path_json( $path, $api->{json}->encode($set) );
+}
+
+sub send_data {
+ my ( $self, $path, $method, $fromfile ) = @_;
+
+ my $config = $self->{config};
+ my $api = $self->{api};
+ my @send_data;
+
+ if ( defined $config->{infile} ) {
+ my $infile = $config->{infile};
+
+ # Slurp it, it's not gonna be >1mb anyway
+ @send_data = <$infile>;
+ }
+ elsif ( defined $fromfile ) {
+ open my $fh, $fromfile or do {
+ $self->error("Can not open file $fromfile: $!");
+ return undef;
+ };
+
+ # Slurp it, it's not gonna be >1mb anyway
+ @send_data = <$fh>;
+ close $fh;
+ }
+ else {
+
+ # Slurp it, it's not gonna be >1mb anyway
+ @send_data = <STDIN>;
+ }
+
+ unless (@send_data) {
+ $self->error(
+"No post data found. Use 'from datafile' or pipes to set post or put data."
+ );
+ return undef;
+ }
+
+ my $send_data = join '', @send_data;
+
+ my $json = $api->{json}->decode($send_data);
+
+ if ( ref $json eq 'ARRAY' && @$json && ref $json->[0] ne 'HASH' ) {
+ $self->verbose('Transforming array style JSON into an hash style one');
+ my %json = @$json;
+ $json = \%json;
+ }
+
+ $self->out_json(
+ $api->send_path_json( $path, $api->{json}->encode($json), undef, $method )
+ );
+
+ return undef;
+}
+
+sub verify {
+ my ($self) = @_;
+ my $api = $self->{api};
+
+ $self->out_json( $api->post_verify_json() );
+}
+
+sub restart {
+ my ($self) = @_;
+ my $api = $self->{api};
+
+ $self->out_json( $api->post_restart_json() );
+}
+
+1;
diff --git a/debian/mon/usr/share/mon/lib/MAPI/RESTlos.pm b/debian/mon/usr/share/mon/lib/MAPI/RESTlos.pm
new file mode 100644
index 0000000..d13ecce
--- /dev/null
+++ b/debian/mon/usr/share/mon/lib/MAPI/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;
diff --git a/debian/mon/usr/share/mon/lib/MAPI/Syslogger.pm b/debian/mon/usr/share/mon/lib/MAPI/Syslogger.pm
new file mode 100644
index 0000000..9292085
--- /dev/null
+++ b/debian/mon/usr/share/mon/lib/MAPI/Syslogger.pm
@@ -0,0 +1,77 @@
+package MON::Syslogger;
+
+use strict;
+use warnings;
+use v5.10;
+use autodie;
+
+use Unix::Syslog qw(:macros :subs);
+use Scalar::Util qw(looks_like_number);
+
+sub new {
+ my ( $class, %opts ) = @_;
+
+ my $self = bless \%opts, $class;
+
+ $self->init();
+
+ return $self;
+}
+
+sub init {
+ my ($self) = @_;
+
+ my $options = $self->{options};
+ $options->store($self);
+
+ if ( exists $self->{syslog} && $self->{syslog} ne '0' ) {
+ $self->{enable} = 1;
+
+ }
+ elsif ( exists $ENV{MON_SYSLOG} && $ENV{MON_SYSLOG} ne '0' ) {
+ $self->{enable} = 1;
+
+ }
+ else {
+ $self->{enable} = 0;
+ }
+
+ return undef;
+}
+
+sub logg {
+ my ( $self, $level, @msgs ) = @_;
+
+ return undef unless $self->{enable};
+
+ openlog $0, LOG_PID, LOG_LOCAL0;
+
+ s/\n/ /g for @msgs;
+
+ given ($level) {
+ when ('debug') {
+ syslog LOG_DEBUG, $_ for @msgs;
+ }
+ when ('warning') {
+ syslog LOG_WARNING, $_ for @msgs;
+ }
+ when ('error') {
+ syslog LOG_ERR, $_ for @msgs;
+ }
+ when ('notice') {
+ syslog LOG_NOTICE, $_ for @msgs;
+ }
+ when ('info') {
+ syslog LOG_INFO, $_ for @msgs;
+ }
+ default {
+ $self->logg( 'info', @msgs )
+ }
+ }
+
+ closelog
+
+ return undef;
+}
+
+1;
diff --git a/debian/mon/usr/share/mon/lib/MAPI/Utils.pm b/debian/mon/usr/share/mon/lib/MAPI/Utils.pm
new file mode 100644
index 0000000..791af8e
--- /dev/null
+++ b/debian/mon/usr/share/mon/lib/MAPI/Utils.pm
@@ -0,0 +1,80 @@
+package MON::Utils;
+
+use strict;
+use warnings;
+use v5.10;
+use autodie;
+
+use Data::Dumper;
+use Exporter;
+
+use base 'Exporter';
+
+our @EXPORT = qw (
+ d
+ dumper
+ get_version
+ isin
+ newline
+ notnull
+ null
+ remove_spaces
+ say
+ sum
+ trim
+);
+
+sub say (@) { print "$_\n" for @_; return undef }
+sub newline () { say ''; return undef }
+sub sum (@) { my $sum = 0; $sum += $_ for @_; return $sum }
+sub null ($) { defined $_[0] ? $_[0] : 0 }
+sub notnull ($) { $_[0] != 0 ? $_[0] : 1 }
+sub dumper (@) { die Dumper @_ }
+sub d (@) { dumper @_ }
+
+sub isin ($@) {
+ my ( $elem, @list ) = @_;
+
+ for (@list) {
+ return 1 if $_ eq $elem;
+ }
+
+ return 0;
+}
+
+sub trim ($) {
+ my $trimit = shift;
+
+ $trimit =~ s/^[\s\t]+//;
+ $trimit =~ s/[\s\t]+$//;
+
+ return $trimit;
+}
+
+sub remove_spaces ($) {
+ my $str = shift;
+
+ $str =~ s/[\s\t]//g;
+
+ return $str;
+}
+
+sub get_version () {
+ my $versionfile = do {
+ if ( -f '.version' ) {
+ '.version';
+ }
+ else {
+ '/usr/share/mon/version';
+ }
+ };
+
+ open my $fh, $versionfile or error("$!: $versionfile");
+ my $version = <$fh>;
+ close $fh;
+
+ chomp $version;
+ return $version;
+}
+
+1;
diff --git a/debian/mon/usr/share/mon/version b/debian/mon/usr/share/mon/version
new file mode 100644
index 0000000..cb2b00e
--- /dev/null
+++ b/debian/mon/usr/share/mon/version
@@ -0,0 +1 @@
+3.0.1
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 0000000..b760bee
--- /dev/null
+++ b/debian/rules
@@ -0,0 +1,13 @@
+#!/usr/bin/make -f
+# -*- makefile -*-
+# Sample debian/rules that uses debhelper.
+# This file was originally written by Joey Hess and Craig Small.
+# As a special exception, when this file is copied by dh-make into a
+# dh-make output file, you may use that output file without restriction.
+# This special exception was added by Craig Small in version 0.37 of dh-make.
+
+# Uncomment this to turn on verbose mode.
+#export DH_VERBOSE=1
+
+%:
+ dh $@
diff --git a/docs/README.txt b/docs/README.txt
new file mode 100644
index 0000000..098e8c2
--- /dev/null
+++ b/docs/README.txt
@@ -0,0 +1 @@
+Only edit the .pod file, and run the 'documentaion' Makefile target.
diff --git a/docs/mon.1 b/docs/mon.1
new file mode 100644
index 0000000..3c5e2da
--- /dev/null
+++ b/docs/mon.1
@@ -0,0 +1,546 @@
+.\" Automatically generated by Pod::Man 2.25 (Pod::Simple 3.16)
+.\"
+.\" Standard preamble:
+.\" ========================================================================
+.de Sp \" Vertical space (when we can't use .PP)
+.if t .sp .5v
+.if n .sp
+..
+.de Vb \" Begin verbatim text
+.ft CW
+.nf
+.ne \\$1
+..
+.de Ve \" End verbatim text
+.ft R
+.fi
+..
+.\" Set up some character translations and predefined strings. \*(-- will
+.\" give an unbreakable dash, \*(PI will give pi, \*(L" will give a left
+.\" double quote, and \*(R" will give a right double quote. \*(C+ will
+.\" give a nicer C++. Capital omega is used to do unbreakable dashes and
+.\" therefore won't be available. \*(C` and \*(C' expand to `' in nroff,
+.\" nothing in troff, for use with C<>.
+.tr \(*W-
+.ds C+ C\v'-.1v'\h'-1p'\s-2+\h'-1p'+\s0\v'.1v'\h'-1p'
+.ie n \{\
+. ds -- \(*W-
+. ds PI pi
+. if (\n(.H=4u)&(1m=24u) .ds -- \(*W\h'-12u'\(*W\h'-12u'-\" diablo 10 pitch
+. if (\n(.H=4u)&(1m=20u) .ds -- \(*W\h'-12u'\(*W\h'-8u'-\" diablo 12 pitch
+. ds L" ""
+. ds R" ""
+. ds C` ""
+. ds C' ""
+'br\}
+.el\{\
+. ds -- \|\(em\|
+. ds PI \(*p
+. ds L" ``
+. ds R" ''
+'br\}
+.\"
+.\" Escape single quotes in literal strings from groff's Unicode transform.
+.ie \n(.g .ds Aq \(aq
+.el .ds Aq '
+.\"
+.\" If the F register is turned on, we'll generate index entries on stderr for
+.\" titles (.TH), headers (.SH), subsections (.SS), items (.Ip), and index
+.\" entries marked with X<> in POD. Of course, you'll have to process the
+.\" output yourself in some meaningful fashion.
+.ie \nF \{\
+. de IX
+. tm Index:\\$1\t\\n%\t"\\$2"
+..
+. nr % 0
+. rr F
+.\}
+.el \{\
+. de IX
+..
+.\}
+.\"
+.\" Accent mark definitions (@(#)ms.acc 1.5 88/02/08 SMI; from UCB 4.2).
+.\" Fear. Run. Save yourself. No user-serviceable parts.
+. \" fudge factors for nroff and troff
+.if n \{\
+. ds #H 0
+. ds #V .8m
+. ds #F .3m
+. ds #[ \f1
+. ds #] \fP
+.\}
+.if t \{\
+. ds #H ((1u-(\\\\n(.fu%2u))*.13m)
+. ds #V .6m
+. ds #F 0
+. ds #[ \&
+. ds #] \&
+.\}
+. \" simple accents for nroff and troff
+.if n \{\
+. ds ' \&
+. ds ` \&
+. ds ^ \&
+. ds , \&
+. ds ~ ~
+. ds /
+.\}
+.if t \{\
+. ds ' \\k:\h'-(\\n(.wu*8/10-\*(#H)'\'\h"|\\n:u"
+. ds ` \\k:\h'-(\\n(.wu*8/10-\*(#H)'\`\h'|\\n:u'
+. ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'^\h'|\\n:u'
+. ds , \\k:\h'-(\\n(.wu*8/10)',\h'|\\n:u'
+. ds ~ \\k:\h'-(\\n(.wu-\*(#H-.1m)'~\h'|\\n:u'
+. ds / \\k:\h'-(\\n(.wu*8/10-\*(#H)'\z\(sl\h'|\\n:u'
+.\}
+. \" troff and (daisy-wheel) nroff accents
+.ds : \\k:\h'-(\\n(.wu*8/10-\*(#H+.1m+\*(#F)'\v'-\*(#V'\z.\h'.2m+\*(#F'.\h'|\\n:u'\v'\*(#V'
+.ds 8 \h'\*(#H'\(*b\h'-\*(#H'
+.ds o \\k:\h'-(\\n(.wu+\w'\(de'u-\*(#H)/2u'\v'-.3n'\*(#[\z\(de\v'.3n'\h'|\\n:u'\*(#]
+.ds d- \h'\*(#H'\(pd\h'-\w'~'u'\v'-.25m'\f2\(hy\fP\v'.25m'\h'-\*(#H'
+.ds D- D\\k:\h'-\w'D'u'\v'-.11m'\z\(hy\v'.11m'\h'|\\n:u'
+.ds th \*(#[\v'.3m'\s+1I\s-1\v'-.3m'\h'-(\w'I'u*2/3)'\s-1o\s+1\*(#]
+.ds Th \*(#[\s+2I\s-2\h'-\w'I'u*3/5'\v'-.3m'o\v'.3m'\*(#]
+.ds ae a\h'-(\w'a'u*4/10)'e
+.ds Ae A\h'-(\w'A'u*4/10)'E
+. \" corrections for vroff
+.if v .ds ~ \\k:\h'-(\\n(.wu*9/10-\*(#H)'\s-2\u~\d\s+2\h'|\\n:u'
+.if v .ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'\v'-.4m'^\v'.4m'\h'|\\n:u'
+. \" for low resolution devices (crt and lpr)
+.if \n(.H>23 .if \n(.V>19 \
+\{\
+. ds : e
+. ds 8 ss
+. ds o a
+. ds d- d\h'-1'\(ga
+. ds D- D\h'-1'\(hy
+. ds th \o'bp'
+. ds Th \o'LP'
+. ds ae ae
+. ds Ae AE
+.\}
+.rm #[ #] #H #V #F C
+.\" ========================================================================
+.\"
+.IX Title "MON 1"
+.TH MON 1 "2015-01-02" "mon 3.0.1" "User Commands"
+.\" For nroff, turn off justification. Always turn off hyphenation; it makes
+.\" way too many mistakes in technical documents.
+.if n .ad l
+.nh
+.SH "NAME"
+mon \- A Humble Monitoring API Tool
+.SH "SYNOPSIS"
+.IX Header "SYNOPSIS"
+m is a synonym of the mon command. You can use either command, but m is shorter to type.
+.PP
+.Vb 1
+\& m [OPTIONS] QUERY [OPTIONS]
+.Ve
+.PP
+mi is a synomym for mon \-\-interactive \-\-meta
+.PP
+.Vb 1
+\& mi [OPTIONS]
+.Ve
+.SS "Where \s-1OPTIONS\s0 can be one (or several) of:"
+.IX Subsection "Where OPTIONS can be one (or several) of:"
+.IP "\-\-config=VAL or \-c=VAL" 4
+.IX Item "--config=VAL or -c=VAL"
+Specifies a config file to read instead the default ones.
+.IP "\-\-debug or \-D" 4
+.IX Item "--debug or -D"
+Prints out extra debugging infos during execution. This option also implies \-\-verbose. \s-1CAUTION:\s0 This switch does not work together with the shell auto completion.
+.IP "\-\-dry or \-d" 4
+.IX Item "--dry or -d"
+Does not modify any object via the \s-1API\s0, read only operations only.
+.IP "\-\-errfile=PATH or \-E=PATH" 4
+.IX Item "--errfile=PATH or -E=PATH"
+If mon is used it is usefull to track if the last mon invocation exited with an error. \s-1PATH\s0 specifies the full path to a status file to be written if mon exists with an error.
+.Sp
+Puppet can check for that file and can re-try the same operation the next run.
+.Sp
+Mon deletes that file automatically after the next successfull run.
+.IP "\-\-help or \-h" 4
+.IX Item "--help or -h"
+Implies a \-\-dry. Also prints out all available options.
+.IP "\-\-interactive or \-i" 4
+.IX Item "--interactive or -i"
+Starts mon in interactive mode. Prefix a command with '!' to run it via shell, e.g. '!ls /tmp'.
+.IP "\-\-meta or \-m" 4
+.IX Item "--meta or -m"
+By default mon does not show any meta (aka nagios custom variables) in its \s-1JSON\s0 output. Those are all variables starting with an underscore (e.g. _WORKER). One exception is the 'edit' operation of mon, it always shows all the meta variables.
+.Sp
+The meta switch makes mon to display also all meta vairables all the time.
+.IP "\-\-nocolor or \-n" 4
+.IX Item "--nocolor or -n"
+By default mon prints out some text in colors. Use this switch to disable that. Or use an environment variable to do that (see \s-1ENVIRONMENT\s0 \s-1VARIABLES\s0 below).
+.IP "\-\-quiet or \-q" 4
+.IX Item "--quiet or -q"
+Quiet mode. No output at all. This also implies \-\-debug=0, \-\-verbose=0, \-\-nocolor.
+.IP "\-\-syslog or \-s" 4
+.IX Item "--syslog or -s"
+Loggs stuff to syslog. See later in this manpage for info more about this.
+.IP "\-\-unique or \-u" 4
+.IX Item "--unique or -u"
+Prints only unique entries in getfmt.
+.IP "\-\-verbose or \-v" 4
+.IX Item "--verbose or -v"
+Prints out extra infos during execution. \s-1CAUTION:\s0 This switch does not work together with the shell auto completion.
+.IP "\-\-version or \-V" 4
+.IX Item "--version or -V"
+Prints out program version.
+.IP "\-\-foo.bar=value" 4
+.IX Item "--foo.bar=value"
+In addition it is possible to overwrite all values of the mon.conf via command line interface. E.g. \-\-restlos.api.port=10043 will overwrite the api port (ignores the value of the mon.conf).
+.Sp
+These keys must be in 'dot\-separated' format.
+.PP
+An option can be written at the beginning or at the end of each command.
+.SS "Where \s-1QUERY\s0 can be one one of:"
+.IX Subsection "Where QUERY can be one one of:"
+.Vb 10
+\& delete CATEGORY [where FIELD1 OP VALUE1 [and ...]]
+\& edit|view CATEGORY [where FIELD1 OP VALUE1 [and ...]]
+\& get CATEGORY [where FIELD1 OP VALUE1 [and ...]]
+\& get CATEGORY [where FIELD1 OP VALUE1 [and ...]] > datafile.json
+\& getfmt FORMAT CATEGORY [where FIELD1 OP VALUE1 [and ...]]
+\& getfmt FORMAT CATEGORY [where FIELD1 OP VALUE1 [and ...]] > datafile
+\& insert CATEGORY set FIELD1 = VALUE1 [and FIELD2 = VALUE2 ...]
+\& post CATEGORY < datafile.json
+\& post CATEGORY from datafile.json
+\& put CATEGORY < datafile.json
+\& put CATEGORY from datafile.json
+\& update CATEGORY delete FIELD1 [and FIELD2 ...]
+\& where FIELD3 OP VALUE3 [and ...]]]
+\& update CATEGORY set FIELD1 = FORMAT [and FIELD2 =
+\& FORMAT2 ...] where FIELD3 OP VALUE3 [and ...]]]
+\& verify|restart|reload [OPTIONS]
+.Ve
+.PP
+\fIShortcut versions of the commands above are:\fR
+.IX Subsection "Shortcut versions of the commands above are:"
+.PP
+.Vb 10
+\& a for and
+\& d for delete
+\& e for edit
+\& f for getfmt
+\& g for get
+\& i for insert
+\& l and ~ for like
+\& p for post
+\& r for reload/restart
+\& s for set
+\& t for put
+\& u for update
+\& v for view
+\& y for verify
+\& : for where
+\& == for eq
+\& != for ne
+\& =~ for matches
+\& !~ for nmatches
+.Ve
+.PP
+They don't show up in the online help in order not to mess it up.
+.SS "And \s-1OP\s0 can be:"
+.IX Subsection "And OP can be:"
+.IP "like" 4
+.IX Item "like"
+Like is the fastest operator and will be used within the query string against the monitoring \s-1API\s0 itself.
+.Sp
+Example:
+ m get host where host_name like testfe01
+.Sp
+will result in a query string /host?host_name=testfe01. All other operators Pre-fetch the results from the \s-1API\s0 and filter the response JSONs.
+.IP "matches" 4
+.IX Item "matches"
+Can be used to use perl regex to filter the fields.
+.Sp
+Example:
+ m get host where host_name matches 'mfad\-adfe\ed\ed\e.*\e.cinetic.de'
+.IP "nmatches" 4
+.IX Item "nmatches"
+Negation of matches.
+.IP "eq" 4
+.IX Item "eq"
+The specific field must be exactly as specified.
+.Sp
+Example:
+ m get host where host_name eq 'mfad\-adfe10.example.com'
+.IP "ne" 4
+.IX Item "ne"
+Negation of eq
+.IP "lt, le, gt, ge" 4
+.IX Item "lt, le, gt, ge"
+The specific field must match (numerically) as specified.
+.Sp
+Example:
+ m get host where host_name lt 10
+.Sp
+retrieves all hosts with its host number lower than 10.
+.Sp
+Example:
+ m get host where host_name gt 10 and host_name le 20 and host_name like server
+.Sp
+retrieves all server hosts between 10 up to 20, which are mfad\-adfe{11..20} actually.
+.SS "And \s-1FORMAT\s0 can be:"
+.IX Subsection "And FORMAT can be:"
+.IP "A string" 4
+.IX Item "A string"
+Example:
+ m update host set name = foo where host_name like testfe01
+.IP "A string with special chars" 4
+.IX Item "A string with special chars"
+Example:
+ m update host set name = 'foo! bar% baz$' where host_name like testfe01
+.IP "A mon variable (uses a value of the current object)" 4
+.IX Item "A mon variable (uses a value of the current object)"
+Examples:
+.Sp
+.Vb 1
+\& m update host set name = \*(Aq$host_name\*(Aq where host_name like testfe01
+\&
+\& m update host set name = \*(Aq${host_name}foo\*(Aq where host_name like testfe01
+.Ve
+.Sp
+Notice: This actually uses the host_name value of the current host object being modified. It can be done with any value of this object.
+.IP "A string with variables expanded by the shell" 4
+.IX Item "A string with variables expanded by the shell"
+Example:
+ m update host set name = \*(L"$shell_expanded \e$host_name\*(R" where host_name like testfe01b
+.Sp
+Notice: In double quotes you must escape the variable if you want to use a mon variable. It is possible to use @ instead to avoid cryptic escape sequences.
+.Sp
+.Vb 1
+\& m update host set name = "$shell_expanded @host_name" where host_name like testfe01b
+.Ve
+.Sp
+It also works this way:
+.Sp
+.Vb 1
+\& m update host set name = "$shell_expanded @{host_name}foo" where host_name like testfe01b
+.Ve
+.IP "A common use case" 4
+.IX Item "A common use case"
+.Vb 1
+\& m update host set name = @host_name where host_name like testfe01
+.Ve
+.ie n .IP "Some ""encrypted"" example" 4
+.el .IP "Some ``encrypted'' example" 4
+.IX Item "Some encrypted example"
+.Vb 1
+\& m update host set _FOO = "@{host_name}knurks${bash_variable}\e$foo\*(Aq where host_name like testfe01
+.Ve
+.IP "Or via getfmt" 4
+.IX Item "Or via getfmt"
+.Vb 1
+\& m getfmt "Host: @host_name" host where host_name like testfe01
+.Ve
+.Sp
+One special case is the following:
+.Sp
+.Vb 1
+\& m getfmt "Host: @HOSTNAME" host where host_name like testfe01
+.Ve
+.Sp
+which explicitly turns host_name which may be a \s-1FQDN\s0 to a host name.
+.SH "CONFIG"
+.IX Header "CONFIG"
+Create a config file by using the the sample configuration file \fI/usr/share/mon/examples/mon.conf.sample\fR into one of the following (or into several places):
+.PP
+.Vb 4
+\& /etc/mon.conf
+\& /etc/mon.d/*.conf
+\& ~/.mon.conf
+\& ~/.mon.d/*.conf
+.Ve
+.PP
+The last config file always overwrites the values configured in the previous config files. The password can be specified in plain text in restlos.auth.password. If that does not exist it can be in restlos.auth.password.enc but Base64 encoded. Example:
+.PP
+.Vb 1
+\& bash \-c \*(Aqread \-s PASSWORD; tr \-d "\en" <<< "$PASSWORD" | base64\*(Aq
+.Ve
+.PP
+This can be overwritten with the \s-1MON_CONFIG\s0 environment variable or the \-\-config= or \-c= switch.
+.PP
+It's also possible to overwrite each single config line via command line option (see \-\-foo.bar=value above).
+.PP
+Some configuration options also support default values. Read the comments of the sample config file to find out more about that.
+.SH "STDOUT and STDERR"
+.IX Header "STDOUT and STDERR"
+\&\s-1JSON\s0 output is always printed to \s-1STDOUT\s0. Makes it easier to redirect it into a file. All other output is always printed to \s-1STDERR\s0, so it's not interfering with the \s-1JSON\s0 stuff.
+.SH "JSON BACKUPS"
+.IX Header "JSON BACKUPS"
+Mon writes backups of the \s-1JSON\s0 data before data is going to be manipulated into the backups.dir directory. Backups older than backups.keep.days days will be deleted on each run automatically, thus the disk space and inodes should not be a problem.
+.PP
+Backup file names are in the form of
+.PP
+.Vb 1
+\& backup_%Y%m%d_%H%M%S_CATEGORY.json
+.Ve
+.PP
+To recover data just do something like this:
+.PP
+.Vb 2
+\& vim ~/.mon/BACKFILE # For the case you want to edit some stuff
+\& m post CATEGORY < ~/.mon/BACKFILE
+.Ve
+.PP
+Set backups.disable to 1 to disable backups.
+.SH ""
+.IX Header ""
+\&\s-1ZSH\s0 users can copy or include the following file to have shell auto completion: \fI/usr/share/mon/contrib/zsh/_mon.zsh\fR. You can add \fI/usr/share/mon/contrib/zsh\fR to the \s-1FPATH\s0 variable and run \fBcompinit m mon\fR.
+.PP
+There is nothing like that for the Bash atm.
+=head1 \s-1ZSH\s0 \s-1AUTO\s0 \s-1COMPLETION\s0
+.PP
+\&\s-1ZSH\s0 users can copy or include the following file to have shell auto completion: \fI/usr/share/mon/contrib/zsh/_mon.zsh\fR. You can add \fI/usr/share/mon/contrib/zsh\fR to the \s-1FPATH\s0 variable and run \fBcompinit m mon\fR.
+.PP
+There is nothing like that for the Bash atm.
+.SH "ENVIRONMENT VARIABLES"
+.IX Header "ENVIRONMENT VARIABLES"
+.SS "\s-1COLOR\s0 \s-1OUTPUT\s0"
+.IX Subsection "COLOR OUTPUT"
+By default mon uses Term::ANSIColor to produce colorful text output. To disable that just set the \s-1MON_COLORFUL\s0 environment variable to 0. It's not possible to specify this in a config file because in verbose mode there is stuff printed already before parsing it.
+.SS "\s-1SSL\s0 \s-1CA\s0 \s-1CERTIFICATE\s0"
+.IX Subsection "SSL CA CERTIFICATE"
+For restlos.api.host \fI./ca.pem\fR or \fI/etc/ssl/certs/ca.pem\fR or \fI/usr/share/mon/ca.pem\fR is used (the first \s-1CA\s0 file found actually). Alternatively point the \s-1HTTPS_CA_FILE\s0 environment variable to the \s-1CA\s0 file to use.
+.PP
+The file \fI/etc/ssl/certs/ca.pem\fR actually comes from the recommended package dependency ca-root-cert, which should be in the Unitix deb repository.
+.SS "\s-1SYSLOG\s0"
+.IX Subsection "SYSLOG"
+it's possible to set the \s-1MON_SYSLOG\s0 environment variable to a value != to logg to syslog. Mon always uses \s-1LOG_LOCAL0\s0.
+.SH "EXIT CODE"
+.IX Header "EXIT CODE"
+.IP "0" 4
+Mon terminates without any error.
+.IP "2" 4
+.IX Item "2"
+The \s-1API\s0 itself terminates with an error (e.g. syntax error).
+.IP "3" 4
+.IX Item "3"
+Some hard error raised by mon itself.
+.PP
+All other exit codes are undefined and/or caused by the autodie Perl module.
+.SH "INPUT JSON FORMAT"
+.IX Header "INPUT JSON FORMAT"
+The mon supports everything that the RESTlos \s-1API\s0 supports as valid \s-1JSON\s0 input. In addition mon also supports to insert a single object in list style format.
+.PP
+Example:
+.PP
+.Vb 1
+\& [ "address", "172.19.184.14", "host_name", "mfad\-adfe01.example.com" ]
+.Ve
+.PP
+Will be interpreted by mon as
+.PP
+.Vb 1
+\& { "address" : "172.19.184.14", "host_name" : "mfad\-adfe01.example.com" }
+.Ve
+.PP
+and pushed this way into the \s-1API\s0.
+.SH "MORE EXAMPLES"
+.IX Header "MORE EXAMPLES"
+Get a list of possible commands
+.PP
+.Vb 1
+\& m
+.Ve
+.PP
+Get a list of possible categories
+.PP
+.Vb 1
+\& m get
+.Ve
+.PP
+Get all defined category objects (e.g. 'mon get host' gets all hosts)
+.PP
+.Vb 1
+\& m get CATEGORY
+.Ve
+.PP
+Print notice with all possible fields
+.PP
+.Vb 1
+\& m get CATEGORY where
+.Ve
+.PP
+Get some stuff
+.PP
+.Vb 1
+\& m get CATEGORY where FIELDNAME like VALUE [and FIELDNAME2 like VALUE2 ...]
+.Ve
+.PP
+Update objects per \s-1POST\s0 request (e.g. mon post contact < pbuetow.json)
+.PP
+.Vb 1
+\& m post CATEGORY < object.json
+.Ve
+.PP
+Get some stuff, open the results in \f(CW$EDITOR\fR (vim by default), commit the changes back via put.
+.PP
+.Vb 1
+\& m edit CATEGORY where FIELDNAME like VALUE [and FIELDNAME2 like VALUE2 ...]
+.Ve
+.PP
+Get some stuff, open the results in \f(CW$PAGER\fR (view by default), just to see in read only mode.
+.PP
+.Vb 1
+\& m view CATEGORY where FIELDNAME like VALUE [and FIELDNAME2 like VALUE2 ...]
+.Ve
+.PP
+Validate the current monitoring configuration
+.PP
+.Vb 1
+\& m verify
+.Ve
+.PP
+Restart/reload the monitoring configuration by restarting the monitoring core. Validation of the configuration is done by the monitoring \s-1API\s0. On failure the previous version will be rolled back automatically by the \s-1API\s0.
+.PP
+.Vb 1
+\& m restart
+.Ve
+.PP
+Run a command in verbose mode
+.PP
+.Vb 1
+\& m verbose get
+.Ve
+.PP
+Fetch all categories
+.PP
+.Vb 1
+\& ( m get 2>&1 ) | while read category; do m get $category > $category.json; done
+.Ve
+.PP
+Delete all contacts with alias like Foo
+.PP
+.Vb 1
+\& m delete contact where alias like Foo
+.Ve
+.PP
+Update fields of an existing object
+.PP
+.Vb 1
+\& m update contact set alias = "Paul Buetow" and _CUSTOM_NEW = "foo" where alias like Buetow
+.Ve
+.PP
+Create some fields, and delete them again
+.PP
+.Vb 1
+\& m update contact set _FOO = "Master of the Universe" and _BAR = "Beer" where email like 1und1
+\&
+\& m update contact delete _FOO and _BAR where email like 1und1
+.Ve
+.PP
+Insert a new contact (raises an error if contact already exists)
+.PP
+.Vb 1
+\& m insert contact set name = "Master of the Universe" and _BAR = "Beer"
+.Ve
+.SH "AUTHOR"
+.IX Header "AUTHOR"
+Paul Buetow \- <paul@buetow.org>
diff --git a/docs/mon.1.gz b/docs/mon.1.gz
new file mode 100644
index 0000000..39568b8
--- /dev/null
+++ b/docs/mon.1.gz
Binary files differ
diff --git a/docs/mon.pod b/docs/mon.pod
new file mode 100644
index 0000000..ad67691
--- /dev/null
+++ b/docs/mon.pod
@@ -0,0 +1,409 @@
+=head1 NAME
+
+mon - A Humble Monitoring API Tool for https://github.com/Crapworks/RESTlos
+
+=head1 SYNOPSIS
+
+m is a synonym of the mon command. You can use either command, but m is shorter to type.
+
+ m [OPTIONS] QUERY [OPTIONS]
+
+mi is a synomym for mon --interactive --meta
+
+ mi [OPTIONS]
+
+=head2 Where OPTIONS can be one (or several) of:
+
+=over
+
+=item --config=VAL or -c=VAL
+
+Specifies a config file to read instead the default ones.
+
+=item --debug or -D
+
+Prints out extra debugging infos during execution. This option also implies --verbose. CAUTION: This switch does not work together with the shell auto completion.
+
+=item --dry or -d
+
+Does not modify any object via the API, read only operations only.
+
+=item --errfile=PATH or -E=PATH
+
+If mon is used it is usefull to track if the last mon invocation exited with an error. PATH specifies the full path to a status file to be written if mon exists with an error.
+
+Puppet can check for that file and can re-try the same operation the next run.
+
+Mon deletes that file automatically after the next successfull run.
+
+=item --help or -h
+
+Implies a --dry. Also prints out all available options.
+
+=item --interactive or -i
+
+Starts mon in interactive mode. Prefix a command with '!' to run it via shell, e.g. '!ls /tmp'.
+
+=item --meta or -m
+
+By default mon does not show any meta (aka nagios custom variables) in its JSON output. Those are all variables starting with an underscore (e.g. _WORKER). One exception is the 'edit' operation of mon, it always shows all the meta variables.
+
+The meta switch makes mon to display also all meta vairables all the time.
+
+=item --nocolor or -n
+
+By default mon prints out some text in colors. Use this switch to disable that. Or use an environment variable to do that (see ENVIRONMENT VARIABLES below).
+
+=item --quiet or -q
+
+Quiet mode. No output at all. This also implies --debug=0, --verbose=0, --nocolor.
+
+=item --syslog or -s
+
+Loggs stuff to syslog. See later in this manpage for info more about this.
+
+=item --unique or -u
+
+Prints only unique entries in getfmt.
+
+=item --verbose or -v
+
+Prints out extra infos during execution. CAUTION: This switch does not work together with the shell auto completion.
+
+=item --version or -V
+
+Prints out program version.
+
+=item --foo.bar=value
+
+In addition it is possible to overwrite all values of the mon.conf via command line interface. E.g. --restlos.api.port=10043 will overwrite the api port (ignores the value of the mon.conf).
+
+These keys must be in 'dot-separated' format.
+
+=back
+
+An option can be written at the beginning or at the end of each command.
+
+=head2 Where QUERY can be one one of:
+
+ delete CATEGORY [where FIELD1 OP VALUE1 [and ...]]
+ edit|view CATEGORY [where FIELD1 OP VALUE1 [and ...]]
+ get CATEGORY [where FIELD1 OP VALUE1 [and ...]]
+ get CATEGORY [where FIELD1 OP VALUE1 [and ...]] > datafile.json
+ getfmt FORMAT CATEGORY [where FIELD1 OP VALUE1 [and ...]]
+ getfmt FORMAT CATEGORY [where FIELD1 OP VALUE1 [and ...]] > datafile
+ insert CATEGORY set FIELD1 = VALUE1 [and FIELD2 = VALUE2 ...]
+ post CATEGORY < datafile.json
+ post CATEGORY from datafile.json
+ put CATEGORY < datafile.json
+ put CATEGORY from datafile.json
+ update CATEGORY delete FIELD1 [and FIELD2 ...]
+ where FIELD3 OP VALUE3 [and ...]]]
+ update CATEGORY set FIELD1 = FORMAT [and FIELD2 =
+ FORMAT2 ...] where FIELD3 OP VALUE3 [and ...]]]
+ verify|restart|reload [OPTIONS]
+
+=head3 Shortcut versions of the commands above are:
+
+ a for and
+ d for delete
+ e for edit
+ f for getfmt
+ g for get
+ i for insert
+ l and ~ for like
+ p for post
+ r for reload/restart
+ s for set
+ t for put
+ u for update
+ v for view
+ y for verify
+ : for where
+ == for eq
+ != for ne
+ =~ for matches
+ !~ for nmatches
+
+They don't show up in the online help in order not to mess it up.
+
+=head2 And OP can be:
+
+=over
+
+=item like
+
+Like is the fastest operator and will be used within the query string against the monitoring API itself.
+
+Example:
+ m get host where host_name like testfe01
+
+will result in a query string /host?host_name=testfe01. All other operators Pre-fetch the results from the API and filter the response JSONs.
+
+=item matches
+
+Can be used to use perl regex to filter the fields.
+
+Example:
+ m get host where host_name matches 'server\d\d\.*\.cinetic.de'
+
+=item nmatches
+
+Negation of matches.
+
+=item eq
+
+The specific field must be exactly as specified.
+
+Example:
+ m get host where host_name eq 'server10.example.com'
+
+=item ne
+
+Negation of eq
+
+=item lt, le, gt, ge
+
+The specific field must match (numerically) as specified.
+
+Example:
+ m get host where host_name lt 10
+
+retrieves all hosts with its host number lower than 10.
+
+Example:
+ m get host where host_name gt 10 and host_name le 20 and host_name like server
+
+retrieves all server hosts between 10 up to 20, which are server{11..20} actually.
+
+=back
+
+=head2 And FORMAT can be:
+
+=over
+
+=item A string
+
+Example:
+ m update host set name = foo where host_name like testfe01
+
+=item A string with special chars
+
+Example:
+ m update host set name = 'foo! bar% baz$' where host_name like testfe01
+
+=item A mon variable (uses a value of the current object)
+
+Examples:
+
+ m update host set name = '$host_name' where host_name like testfe01
+
+ m update host set name = '${host_name}foo' where host_name like testfe01
+
+Notice: This actually uses the host_name value of the current host object being modified. It can be done with any value of this object.
+
+=item A string with variables expanded by the shell
+
+Example:
+ m update host set name = "$shell_expanded \$host_name" where host_name like testfe01b
+
+Notice: In double quotes you must escape the variable if you want to use a mon variable. It is possible to use @ instead to avoid cryptic escape sequences.
+
+ m update host set name = "$shell_expanded @host_name" where host_name like testfe01b
+
+It also works this way:
+
+ m update host set name = "$shell_expanded @{host_name}foo" where host_name like testfe01b
+
+=item A common use case
+
+ m update host set name = @host_name where host_name like testfe01
+
+=item Some "encrypted" example
+
+ m update host set _FOO = "@{host_name}knurks${bash_variable}\$foo' where host_name like testfe01
+
+=item Or via getfmt
+
+ m getfmt "Host: @host_name" host where host_name like testfe01
+
+One special case is the following:
+
+ m getfmt "Host: @HOSTNAME" host where host_name like testfe01
+
+which explicitly turns host_name which may be a FQDN to a host name.
+
+=back
+
+=head1 CONFIG
+
+Create a config file by using the the sample configuration file F</usr/share/mon/examples/mon.conf.sample> into one of the following (or into several places):
+
+ /etc/mon.conf
+ /etc/mon.d/*.conf
+ ~/.mon.conf
+ ~/.mon.d/*.conf
+
+The last config file always overwrites the values configured in the previous config files. The password can be specified in plain text in restlos.auth.password. If that does not exist it can be in restlos.auth.password.enc but Base64 encoded. Example:
+
+ bash -c 'read -s PASSWORD; tr -d "\n" <<< "$PASSWORD" | base64'
+
+This can be overwritten with the MON_CONFIG environment variable or the --config= or -c= switch.
+
+It's also possible to overwrite each single config line via command line option (see --foo.bar=value above).
+
+Some configuration options also support default values. Read the comments of the sample config file to find out more about that.
+
+=head1 STDOUT and STDERR
+
+JSON output is always printed to STDOUT. Makes it easier to redirect it into a file. All other output is always printed to STDERR, so it's not interfering with the JSON stuff.
+
+=head1 JSON BACKUPS
+
+Mon writes backups of the JSON data before data is going to be manipulated into the backups.dir directory. Backups older than backups.keep.days days will be deleted on each run automatically, thus the disk space and inodes should not be a problem.
+
+Backup file names are in the form of
+
+ backup_%Y%m%d_%H%M%S_CATEGORY.json
+
+To recover data just do something like this:
+
+ vim ~/.mon/BACKFILE # For the case you want to edit some stuff
+ m post CATEGORY < ~/.mon/BACKFILE
+
+Set backups.disable to 1 to disable backups.
+
+=head1
+
+ZSH users can copy or include the following file to have shell auto completion: F</usr/share/mon/contrib/zsh/_mon.zsh>. You can add F</usr/share/mon/contrib/zsh> to the FPATH variable and run B<compinit m mon>.
+
+There is nothing like that for the Bash atm.
+=head1 ZSH AUTO COMPLETION
+
+ZSH users can copy or include the following file to have shell auto completion: F</usr/share/mon/contrib/zsh/_mon.zsh>. You can add F</usr/share/mon/contrib/zsh> to the FPATH variable and run B<compinit m mon>.
+
+There is nothing like that for the Bash atm.
+
+=head1 ENVIRONMENT VARIABLES
+
+=head2 COLOR OUTPUT
+
+By default mon uses Term::ANSIColor to produce colorful text output. To disable that just set the MON_COLORFUL environment variable to 0. It's not possible to specify this in a config file because in verbose mode there is stuff printed already before parsing it.
+
+=head2 SSL CA CERTIFICATE
+
+For restlos.api.host F<./ca.pem> or F</etc/ssl/certs/ca.pem> or F</usr/share/mon/ca.pem> is used (the first CA file found actually). Alternatively point the HTTPS_CA_FILE environment variable to the CA file to use.
+
+The file F</etc/ssl/certs/ca.pem> actually comes from the recommended package dependency ca-root-cert, which should be in the Unitix deb repository.
+
+=head2 SYSLOG
+
+it's possible to set the MON_SYSLOG environment variable to a value != to logg to syslog. Mon always uses LOG_LOCAL0.
+
+=head1 EXIT CODE
+
+=over
+
+=item 0
+
+Mon terminates without any error.
+
+=item 2
+
+The API itself terminates with an error (e.g. syntax error).
+
+=item 3
+
+Some hard error raised by mon itself.
+
+=back
+
+All other exit codes are undefined and/or caused by the autodie Perl module.
+
+=head1 INPUT JSON FORMAT
+
+The mon supports everything that the RESTlos API supports as valid JSON input. In addition mon also supports to insert a single object in list style format.
+
+Example:
+
+ [ "address", "172.19.184.14", "host_name", "server01.example.com" ]
+
+Will be interpreted by mon as
+
+ { "address" : "172.19.184.14", "host_name" : "server01.example.com" }
+
+and pushed this way into the API.
+
+=head1 MORE EXAMPLES
+
+Get a list of possible commands
+
+ m
+
+Get a list of possible categories
+
+ m get
+
+Get all defined category objects (e.g. 'mon get host' gets all hosts)
+
+ m get CATEGORY
+
+Print notice with all possible fields
+
+ m get CATEGORY where
+
+Get some stuff
+
+ m get CATEGORY where FIELDNAME like VALUE [and FIELDNAME2 like VALUE2 ...]
+
+Update objects per POST request (e.g. mon post contact < pbuetow.json)
+
+ m post CATEGORY < object.json
+
+Get some stuff, open the results in $EDITOR (vim by default), commit the changes back via put.
+
+ m edit CATEGORY where FIELDNAME like VALUE [and FIELDNAME2 like VALUE2 ...]
+
+Get some stuff, open the results in $PAGER (view by default), just to see in read only mode.
+
+ m view CATEGORY where FIELDNAME like VALUE [and FIELDNAME2 like VALUE2 ...]
+
+Validate the current monitoring configuration
+
+ m verify
+
+Restart/reload the monitoring configuration by restarting the monitoring core. Validation of the configuration is done by the monitoring API. On failure the previous version will be rolled back automatically by the API.
+
+ m restart
+
+Run a command in verbose mode
+
+ m verbose get
+
+Fetch all categories
+
+ ( m get 2>&1 ) | while read category; do m get $category > $category.json; done
+
+Delete all contacts with alias like Foo
+
+ m delete contact where alias like Foo
+
+Update fields of an existing object
+
+ m update contact set alias = "Paul Buetow" and _CUSTOM_NEW = "foo" where alias like Buetow
+
+Create some fields, and delete them again
+
+ m update contact set _FOO = "Master of the Universe" and _BAR = "Beer" where email like 1und1
+
+ m update contact delete _FOO and _BAR where email like 1und1
+
+Insert a new contact (raises an error if contact already exists)
+
+ m insert contact set name = "Master of the Universe" and _BAR = "Beer"
+
+=head1 AUTHOR
+
+Paul Buetow - <paul@buetow.org>
+
+=cut
diff --git a/docs/mon.txt b/docs/mon.txt
new file mode 100644
index 0000000..0800619
--- /dev/null
+++ b/docs/mon.txt
@@ -0,0 +1,394 @@
+NAME
+ mon - A Humble Monitoring API Tool
+
+SYNOPSIS
+ m is a synonym of the mon command. You can use either command, but m is
+ shorter to type.
+
+ m [OPTIONS] QUERY [OPTIONS]
+
+ mi is a synomym for mon --interactive --meta
+
+ mi [OPTIONS]
+
+ Where OPTIONS can be one (or several) of:
+ --config=VAL or -c=VAL
+ Specifies a config file to read instead the default ones.
+
+ --debug or -D
+ Prints out extra debugging infos during execution. This option also
+ implies --verbose. CAUTION: This switch does not work together with
+ the shell auto completion.
+
+ --dry or -d
+ Does not modify any object via the API, read only operations only.
+
+ --errfile=PATH or -E=PATH
+ If mon is used it is usefull to track if the last mon invocation
+ exited with an error. PATH specifies the full path to a status file
+ to be written if mon exists with an error.
+
+ Puppet can check for that file and can re-try the same operation the
+ next run.
+
+ Mon deletes that file automatically after the next successfull run.
+
+ --help or -h
+ Implies a --dry. Also prints out all available options.
+
+ --interactive or -i
+ Starts mon in interactive mode. Prefix a command with '!' to run it
+ via shell, e.g. '!ls /tmp'.
+
+ --meta or -m
+ By default mon does not show any meta (aka nagios custom variables)
+ in its JSON output. Those are all variables starting with an
+ underscore (e.g. _WORKER). One exception is the 'edit' operation of
+ mon, it always shows all the meta variables.
+
+ The meta switch makes mon to display also all meta vairables all
+ the time.
+
+ --nocolor or -n
+ By default mon prints out some text in colors. Use this switch to
+ disable that. Or use an environment variable to do that (see
+ ENVIRONMENT VARIABLES below).
+
+ --quiet or -q
+ Quiet mode. No output at all. This also implies --debug=0,
+ --verbose=0, --nocolor.
+
+ --syslog or -s
+ Loggs stuff to syslog. See later in this manpage for info more about
+ this.
+
+ --unique or -u
+ Prints only unique entries in getfmt.
+
+ --verbose or -v
+ Prints out extra infos during execution. CAUTION: This switch does
+ not work together with the shell auto completion.
+
+ --version or -V
+ Prints out program version.
+
+ --foo.bar=value
+ In addition it is possible to overwrite all values of the mon.conf
+ via command line interface. E.g. --restlos.api.port=10043 will
+ overwrite the api port (ignores the value of the mon.conf).
+
+ These keys must be in 'dot-separated' format.
+
+ An option can be written at the beginning or at the end of each command.
+
+ Where QUERY can be one one of:
+ delete CATEGORY [where FIELD1 OP VALUE1 [and ...]]
+ edit|view CATEGORY [where FIELD1 OP VALUE1 [and ...]]
+ get CATEGORY [where FIELD1 OP VALUE1 [and ...]]
+ get CATEGORY [where FIELD1 OP VALUE1 [and ...]] > datafile.json
+ getfmt FORMAT CATEGORY [where FIELD1 OP VALUE1 [and ...]]
+ getfmt FORMAT CATEGORY [where FIELD1 OP VALUE1 [and ...]] > datafile
+ insert CATEGORY set FIELD1 = VALUE1 [and FIELD2 = VALUE2 ...]
+ post CATEGORY < datafile.json
+ post CATEGORY from datafile.json
+ put CATEGORY < datafile.json
+ put CATEGORY from datafile.json
+ update CATEGORY delete FIELD1 [and FIELD2 ...]
+ where FIELD3 OP VALUE3 [and ...]]]
+ update CATEGORY set FIELD1 = FORMAT [and FIELD2 =
+ FORMAT2 ...] where FIELD3 OP VALUE3 [and ...]]]
+ verify|restart|reload [OPTIONS]
+
+ Shortcut versions of the commands above are:
+ a for and
+ d for delete
+ e for edit
+ f for getfmt
+ g for get
+ i for insert
+ l and ~ for like
+ p for post
+ r for reload/restart
+ s for set
+ t for put
+ u for update
+ v for view
+ y for verify
+ : for where
+ == for eq
+ != for ne
+ =~ for matches
+ !~ for nmatches
+
+ They don't show up in the online help in order not to mess it up.
+
+ And OP can be:
+ like
+ Like is the fastest operator and will be used within the query
+ string against the monitoring API itself.
+
+ Example: m get host where host_name like testfe01
+
+ will result in a query string /host?host_name=testfe01. All other
+ operators Pre-fetch the results from the API and filter the response
+ JSONs.
+
+ matches
+ Can be used to use perl regex to filter the fields.
+
+ Example: m get host where host_name matches
+ 'server\d\d\.*\.cinetic.de'
+
+ nmatches
+ Negation of matches.
+
+ eq The specific field must be exactly as specified.
+
+ Example: m get host where host_name eq 'server10.example.com'
+
+ ne Negation of eq
+
+ lt, le, gt, ge
+ The specific field must match (numerically) as specified.
+
+ Example: m get host where host_name lt 10
+
+ retrieves all hosts with its host number lower than 10.
+
+ Example: m get host where host_name gt 10 and host_name le 20 and
+ host_name like server
+
+ retrieves all server hosts between 10 up to 20, which are
+ server{11..20} actually.
+
+ And FORMAT can be:
+ A string
+ Example: m update host set name = foo where host_name like testfe01
+
+ A string with special chars
+ Example: m update host set name = 'foo! bar% baz$' where host_name
+ like testfe01
+
+ A mon variable (uses a value of the current object)
+ Examples:
+
+ m update host set name = '$host_name' where host_name like testfe01
+
+ m update host set name = '${host_name}foo' where host_name like testfe01
+
+ Notice: This actually uses the host_name value of the current host
+ object being modified. It can be done with any value of this object.
+
+ A string with variables expanded by the shell
+ Example: m update host set name = "$shell_expanded \$host_name"
+ where host_name like testfe01b
+
+ Notice: In double quotes you must escape the variable if you want to
+ use a mon variable. It is possible to use @ instead to avoid
+ cryptic escape sequences.
+
+ m update host set name = "$shell_expanded @host_name" where host_name like testfe01b
+
+ It also works this way:
+
+ m update host set name = "$shell_expanded @{host_name}foo" where host_name like testfe01b
+
+ A common use case
+ m update host set name = @host_name where host_name like testfe01
+
+ Some "encrypted" example
+ m update host set _FOO = "@{host_name}knurks${bash_variable}\$foo' where host_name like testfe01
+
+ Or via getfmt
+ m getfmt "Host: @host_name" host where host_name like testfe01
+
+ One special case is the following:
+
+ m getfmt "Host: @HOSTNAME" host where host_name like testfe01
+
+ which explicitly turns host_name which may be a FQDN to a host name.
+
+CONFIG
+ Create a config file by using the the sample configuration file
+ /usr/share/mon/examples/mon.conf.sample into one of the following (or
+ into several places):
+
+ /etc/mon.conf
+ /etc/mon.d/*.conf
+ ~/.mon.conf
+ ~/.mon.d/*.conf
+
+ The last config file always overwrites the values configured in the
+ previous config files. The password can be specified in plain text in
+ restlos.auth.password. If that does not exist it can be in
+ restlos.auth.password.enc but Base64 encoded. Example:
+
+ bash -c 'read -s PASSWORD; tr -d "\n" <<< "$PASSWORD" | base64'
+
+ This can be overwritten with the MON_CONFIG environment variable or the
+ --config= or -c= switch.
+
+ It's also possible to overwrite each single config line via command line
+ option (see --foo.bar=value above).
+
+ Some configuration options also support default values. Read the
+ comments of the sample config file to find out more about that.
+
+STDOUT and STDERR
+ JSON output is always printed to STDOUT. Makes it easier to redirect it
+ into a file. All other output is always printed to STDERR, so it's not
+ interfering with the JSON stuff.
+
+JSON BACKUPS
+ Mon writes backups of the JSON data before data is going to be
+ manipulated into the backups.dir directory. Backups older than
+ backups.keep.days days will be deleted on each run automatically, thus
+ the disk space and inodes should not be a problem.
+
+ Backup file names are in the form of
+
+ backup_%Y%m%d_%H%M%S_CATEGORY.json
+
+ To recover data just do something like this:
+
+ vim ~/.mon/BACKFILE # For the case you want to edit some stuff
+ m post CATEGORY < ~/.mon/BACKFILE
+
+ Set backups.disable to 1 to disable backups.
+
+
+ ZSH users can copy or include the following file to have shell auto
+ completion: /usr/share/mon/contrib/zsh/_mon.zsh. You can add
+ /usr/share/mon/contrib/zsh to the FPATH variable and run compinit m
+ mon.
+
+ There is nothing like that for the Bash atm. =head1 ZSH AUTO COMPLETION
+
+ ZSH users can copy or include the following file to have shell auto
+ completion: /usr/share/mon/contrib/zsh/_mon.zsh. You can add
+ /usr/share/mon/contrib/zsh to the FPATH variable and run compinit m
+ mon.
+
+ There is nothing like that for the Bash atm.
+
+ENVIRONMENT VARIABLES
+ COLOR OUTPUT
+ By default mon uses Term::ANSIColor to produce colorful text output. To
+ disable that just set the MON_COLORFUL environment variable to 0. It's
+ not possible to specify this in a config file because in verbose mode
+ there is stuff printed already before parsing it.
+
+ SSL CA CERTIFICATE
+ For restlos.api.host ./ca.pem or /etc/ssl/certs/ca.pem or
+ /usr/share/mon/ca.pem is used (the first CA file found actually).
+ Alternatively point the HTTPS_CA_FILE environment variable to the CA
+ file to use.
+
+ The file /etc/ssl/certs/ca.pem actually comes from the recommended
+ package dependency ca-root-cert, which should be in the Unitix deb
+ repository.
+
+ SYSLOG
+ it's possible to set the MON_SYSLOG environment variable to a value !=
+ to logg to syslog. Mon always uses LOG_LOCAL0.
+
+EXIT CODE
+ 0 Mon terminates without any error.
+
+ 2 The API itself terminates with an error (e.g. syntax error).
+
+ 3 Some hard error raised by mon itself.
+
+ All other exit codes are undefined and/or caused by the autodie Perl
+ module.
+
+INPUT JSON FORMAT
+ The mon supports everything that the RESTlos API supports as valid JSON
+ input. In addition mon also supports to insert a single object in list
+ style format.
+
+ Example:
+
+ [ "address", "172.19.184.14", "host_name", "server01.example.com" ]
+
+ Will be interpreted by mon as
+
+ { "address" : "172.19.184.14", "host_name" : "server01.example.com" }
+
+ and pushed this way into the API.
+
+MORE EXAMPLES
+ Get a list of possible commands
+
+ m
+
+ Get a list of possible categories
+
+ m get
+
+ Get all defined category objects (e.g. 'mon get host' gets all hosts)
+
+ m get CATEGORY
+
+ Print notice with all possible fields
+
+ m get CATEGORY where
+
+ Get some stuff
+
+ m get CATEGORY where FIELDNAME like VALUE [and FIELDNAME2 like VALUE2 ...]
+
+ Update objects per POST request (e.g. mon post contact < pbuetow.json)
+
+ m post CATEGORY < object.json
+
+ Get some stuff, open the results in $EDITOR (vim by default), commit the
+ changes back via put.
+
+ m edit CATEGORY where FIELDNAME like VALUE [and FIELDNAME2 like VALUE2 ...]
+
+ Get some stuff, open the results in $PAGER (view by default), just to
+ see in read only mode.
+
+ m view CATEGORY where FIELDNAME like VALUE [and FIELDNAME2 like VALUE2 ...]
+
+ Validate the current monitoring configuration
+
+ m verify
+
+ Restart/reload the monitoring configuration by restarting the monitoring
+ core. Validation of the configuration is done by the monitoring API. On
+ failure the previous version will be rolled back automatically by the
+ API.
+
+ m restart
+
+ Run a command in verbose mode
+
+ m verbose get
+
+ Fetch all categories
+
+ ( m get 2>&1 ) | while read category; do m get $category > $category.json; done
+
+ Delete all contacts with alias like Foo
+
+ m delete contact where alias like Foo
+
+ Update fields of an existing object
+
+ m update contact set alias = "Paul Buetow" and _CUSTOM_NEW = "foo" where alias like Buetow
+
+ Create some fields, and delete them again
+
+ m update contact set _FOO = "Master of the Universe" and _BAR = "Beer" where email like 1und1
+
+ m update contact delete _FOO and _BAR where email like 1und1
+
+ Insert a new contact (raises an error if contact already exists)
+
+ m insert contact set name = "Master of the Universe" and _BAR = "Beer"
+
+AUTHOR
+ Paul Buetow - <paul@buetow.org>
+
diff --git a/lib/MON/Cache.pm b/lib/MON/Cache.pm
new file mode 100644
index 0000000..21b59f5
--- /dev/null
+++ b/lib/MON/Cache.pm
@@ -0,0 +1,55 @@
+package MON::Cache;
+
+use strict;
+use warnings;
+use v5.10;
+use autodie;
+
+use Data::Dumper;
+
+use MON::Display;
+use MON::Config;
+use MON::Utils;
+
+our @ISA = ('MON::Display');
+
+sub new {
+ my ( $class, %opts ) = @_;
+
+ my $self = bless \%opts, $class;
+
+ $self->init();
+
+ return $self;
+}
+
+sub init {
+ my ($self) = @_;
+
+ $self->clear();
+
+ return undef;
+}
+
+sub clear {
+ my ($self) = @_;
+
+ $self->{cache} = {};
+
+ return undef;
+}
+
+sub magic {
+ my ( $self, $key, $sub ) = @_;
+
+ my $cache = $self->{cache};
+
+ if ( exists $cache->{$key} ) {
+ $self->verbose("Delivering '$key' from cache");
+ return $cache->{$key};
+ }
+
+ return $cache->{$key} = $sub->();
+}
+
+1;
diff --git a/lib/MON/Config.pm b/lib/MON/Config.pm
new file mode 100644
index 0000000..dc83911
--- /dev/null
+++ b/lib/MON/Config.pm
@@ -0,0 +1,176 @@
+package MON::Config;
+
+use strict;
+use warnings;
+use v5.10;
+use autodie;
+
+use IO::File;
+use Data::Dumper;
+
+use MON::Display;
+use MON::Utils;
+
+#use MON::Options;
+
+use MIME::Base64 qw( decode_base64 );
+
+our @ISA = ('MON::Display');
+
+sub new {
+ my ( $class, %opts ) = @_;
+
+ my $self = bless \%opts, $class;
+ my $options = $self->{options};
+
+ $options->store_first($self);
+
+ $self->SUPER::init(%opts);
+
+ for ( @{ $options->{unknown} } ) {
+ $self->error("Unknown option: $_");
+ }
+
+ if ( $self->{'config'} ne '' ) {
+ $self->read_config( $self->{'config'} );
+
+ }
+ elsif ( exists $ENV{MON_CONFIG} ) {
+ $self->read_config( $ENV{MON_CONFIG} );
+
+ }
+ else {
+ $self->read_config('/etc/mon.conf');
+ $self->read_config($_) for sort glob("/etc/mon.d/*.conf");
+
+ $self->read_config("$ENV{HOME}/.mon.conf");
+ $self->read_config($_) for sort glob("$ENV{HOME}/.mon.d/*.conf");
+ }
+
+ $options->store_after($self);
+
+ unless ( exists $self->{config_was_read} ) {
+ $self->verbose("No config file found, but this might be OK");
+ }
+
+ $self->_set_defaults();
+
+ return $self;
+}
+
+sub _set_defaults {
+ my ($self) = @_;
+
+ my $set_default = sub {
+ my ( $key, $val ) = @_;
+
+ unless ( exists $self->{$key} ) {
+ $self->{$key} = $val;
+ $self->verbose(
+ "Since $key is not specified setting its default value to $val");
+ }
+ };
+
+ $set_default->( 'backups.dir' => "$ENV{HOME}/.mon" );
+ $set_default->( 'backups.disable' => 1 );
+ $set_default->( 'backups.keep.days' => 7 );
+ $set_default->( 'restlos.api.port' => '443' );
+ $set_default->( 'restlos.api.protocol' => 'https' );
+ $set_default->( 'restlos.auth.realm' => 'Login Required' );
+ $set_default->( 'restlos.auth.username' => $ENV{USER} );
+}
+
+sub read_config {
+ my ( $self, $config_file ) = @_;
+
+ return undef if not defined $config_file or not -f $config_file;
+
+ my $fh = IO::File->new( $config_file, 'r' );
+ $self->error("Could not open file $config_file") unless defined $fh;
+
+ $self->verbose("Reading config $config_file");
+
+ while ( my $line = $fh->getline() ) {
+ next if $line =~ /^#/;
+
+ # Ignore comments
+ $line =~ s/(.*);.*/$1/;
+
+ # Parse only matching lines
+ if ( $line =~ /^(.*):(.*)/ ) {
+ my ( $key, $val ) = ( lc trim $1, trim $2);
+ $self->verbose("Reading conf value $key");
+
+ # Handle ~
+ $val =~ s/~/$ENV{HOME}/g;
+ $self->set( $key, $val );
+ }
+ }
+
+ $fh->close();
+ $self->{config_was_read} = 1;
+
+ return undef;
+}
+
+sub get {
+ my ( $self, $key ) = @_;
+ $key = lc $key;
+
+ $self->{$key} //= do {
+ my $key = uc $key;
+ $key =~ s/\./_/g;
+
+ exists $ENV{$key} ? $ENV{$key} : undef;
+ };
+
+ if ( not exists $self->{$key}
+ or not defined $self->{$key}
+ or $self->{$key} eq '' )
+ {
+ $self->error("$key not configured");
+ }
+
+ return $self->{$key};
+}
+
+sub get_maybe_encoded {
+ my ( $self, $key ) = @_;
+
+ return $self->get($key) if exists $self->{$key};
+
+ $self->error("$key or $key.enc not configured")
+ unless exists $self->{"$key.enc"};
+
+ my $enc = $self->get("$key.enc");
+
+ return decode_base64($enc);
+}
+
+sub bool {
+ my ( $self, $key ) = @_;
+
+ my $val = $self->get($key);
+
+ return $val != 0;
+}
+
+sub array {
+ my ( $self, $key ) = @_;
+
+ my $val = $self->get($key);
+
+ return map { trim $_ } split ',', $val;
+}
+
+sub set {
+ my ( $self, $key, $val ) = @_;
+ $key = lc $key;
+
+ $self->verbose("$key already configured, overwriting it with its new value")
+ if exists $self->{$key};
+
+ return $self->{$key} = $val;
+}
+
+1;
diff --git a/lib/MON/Display.pm b/lib/MON/Display.pm
new file mode 100644
index 0000000..9bf8115
--- /dev/null
+++ b/lib/MON/Display.pm
@@ -0,0 +1,360 @@
+package MON::Display;
+
+use strict;
+use warnings;
+use v5.10;
+use autodie;
+
+use Data::Dumper;
+use Term::ANSIColor;
+
+use MON::Config;
+use MON::JSON;
+use MON::Utils;
+
+our $VERBOSE = 0;
+our $DEBUG = 0;
+our $COLORFUL = 0;
+our $QUIET = 0;
+our $LOGGER = undef;
+our $INTERACTIVE = undef;
+
+sub init {
+ my ( $self, %opts ) = @_;
+
+ $VERBOSE = $self->{'verbose'} == 1;
+ $DEBUG = $self->{'debug'} == 1;
+ $QUIET = $self->{'quiet'} == 1;
+ $LOGGER = $opts{logger};
+ $INTERACTIVE = $opts{interactive};
+
+ $self->{logglevel} = 'info';
+
+ if ( $self->{'nocolor'} == 1 ) {
+ $COLORFUL = 0;
+ }
+ else {
+ $COLORFUL = $ENV{MON_COLORFUL} // 1;
+ }
+
+ $VERBOSE = $DEBUG = $COLORFUL = 0 if $QUIET == 1;
+
+ return undef;
+}
+
+sub is_verbose {
+ my ($self) = @_;
+
+ return $VERBOSE == 1;
+}
+
+sub is_debug {
+ my ($self) = @_;
+
+ return $DEBUG == 1;
+}
+
+sub is_quiet {
+ my ($self) = @_;
+
+ return $QUIET == 1;
+}
+
+sub _display {
+ my ( $self, $msg, $fh, $level ) = @_;
+
+ return undef unless defined $msg;
+
+ $LOGGER->logg( $self->{logglevel}, $msg ) if defined $LOGGER;
+
+ return undef if $QUIET;
+
+ $fh = *STDERR unless defined $fh;
+
+ print $fh $msg;
+
+ return undef;
+}
+
+sub info_no_nl {
+ my ( $self, $msg ) = @_;
+
+ print STDERR color 'bold blue' if $COLORFUL;
+ $self->_display($msg);
+ print STDERR color 'reset' if $COLORFUL;
+
+ return undef;
+}
+
+sub out_json {
+ my ( $self, $out ) = @_;
+
+ return undef unless defined $out;
+ my $config = $self->{config};
+
+ local $, = "\n";
+
+ my $json = MON::JSON->new()->decode($out);
+ my $num_results = ref $json eq 'ARRAY' ? @$json : undef;
+
+ # Don't _display meta aka custom variables unless -m or --meta is specified
+ unless ( $config->{'meta'} ) {
+ if ( ref $json eq 'ARRAY' ) {
+ @$json = map {
+ if ( ref $_ eq 'HASH' )
+ {
+ my $h = $_;
+ delete $h->{$_} for grep /^_/, keys %$h;
+ $h;
+ }
+ else {
+ $_;
+ }
+ } @$json;
+ }
+ }
+
+ # Sort and pretty print all the JSON pretty pretty please
+ unless ( defined $config->{outfile} ) {
+ print MON::JSON->new()->encode_canonical($json) unless $QUIET;
+ }
+ else {
+ my $outfile = $config->{outfile};
+ print $outfile MON::JSON->new()->encode_canonical($json);
+ print STDERR color 'bold green' if $COLORFUL;
+ $self->_display("Wrote JSON to file\n");
+ print STDERR color 'reset' if $COLORFUL;
+ }
+
+ $LOGGER->logg( 'info', JSON->new()->encode($json) ) if defined $LOGGER;
+
+ print STDERR color 'bold green' if $COLORFUL;
+ $self->_display("Found $num_results entries\n") if defined $num_results;
+ print STDERR color 'reset' if $COLORFUL;
+
+ return undef;
+}
+
+sub out_format {
+ my ( $self, $format, $out ) = @_;
+
+ return undef unless defined $out;
+
+ my $config = $self->{config};
+ my $options = $self->{options};
+ my $json = MON::JSON->new()->decode($out);
+ my $num_results = ref $json eq 'ARRAY' ? @$json : undef;
+
+ $self->error("Expected an JSON Array") if ref $json ne 'ARRAY';
+
+ my @vars1 = $format =~ /\$(\w+)/g;
+ my @vars2 = $format =~ /\$\{(\w+)\}/g;
+ my @vars3 = $format =~ /\@(\w+)/g;
+ my @vars4 = $format =~ /\@\{(\w+)\}/g;
+
+ my %vars;
+ $vars{$_} = '' for @vars1, @vars2, @vars3, @vars4;
+ my @out;
+ my %empty;
+
+ for my $obj (@$json) {
+ my %obj_vars = %vars;
+ my $obj_format = $format;
+
+ for my $var ( keys %obj_vars ) {
+ if ( $var eq 'HOSTNAME' ) {
+ my $val = exists $obj->{host_name} ? $obj->{host_name} : '';
+
+ if ( $val eq '' ) {
+ $empty{$var} = 1;
+ }
+ else {
+ $val =~ s/\..*//;
+ }
+
+ $obj_format =~ s/\$$var/$val/g;
+
+ }
+ else {
+ my $val = exists $obj->{$var} ? $obj->{$var} : '';
+ $empty{$var} = 1 if $val eq '';
+
+ $obj_format =~ s/\$$var/$val/g;
+ $obj_format =~ s/\$\{$var\}/$val/g;
+ $obj_format =~ s/\@$var/$val/g;
+ $obj_format =~ s/\@\{$var\}/$val/g;
+ }
+ }
+
+ push @out, $obj_format if $obj_format =~ /^.*\w+.*$/;
+ }
+
+ if (@out) {
+
+ if ( $config->{'unique'} ) {
+ my %lines;
+ @out = grep { exists $lines{$_} ? 0 : ( $lines{$_} = 1 ) } sort @out;
+ $num_results = @out;
+ }
+ else {
+ @out = sort @out;
+ }
+
+ if ( $QUIET == 0 ) {
+ local $, = "\n";
+ print @out;
+ say '';
+ }
+ elsif ( defined $LOGGER ) {
+ $LOGGER->logg( 'info', $_ ) for @out;
+ }
+ }
+
+ $self->warning( "Some objects dont have such a field or have empty strings: "
+ . join( ' ', sort keys %empty ) )
+ if keys %empty;
+
+ print STDERR color 'bold green' if $COLORFUL;
+ $self->_display("Found $num_results entries\n") if defined $num_results;
+ print STDERR color 'reset' if $COLORFUL;
+
+ return undef;
+}
+
+sub info {
+ my ( $self, $msg ) = @_;
+
+ my $str = "$msg\n";
+ $self->{logglevel} = 'info';
+
+ print STDERR color 'bold blue' if $COLORFUL;
+ $self->_display($str);
+ print STDERR color 'reset' if $COLORFUL;
+
+ return undef;
+}
+
+sub nl {
+ my ($self) = @_;
+
+ $self->_display("\n");
+
+ return undef;
+}
+
+sub error {
+ my ( $self, $msg ) = @_;
+
+ $self->error_no_exit($msg);
+
+ exit 3 unless $INTERACTIVE;
+}
+
+sub error_no_exit {
+ my ( $self, $msg ) = @_;
+
+ $self->{logglevel} = 'warning';
+ print STDERR color 'bold red' if $COLORFUL;
+ $self->_display( "! ERROR: $msg\n", *STDERR );
+ print STDERR color 'reset' if $COLORFUL;
+
+ return undef;
+}
+
+sub possible {
+ my ( $self, @params ) = @_;
+
+ my $config = $self->{config};
+ my $options = $self->{options};
+
+ push @params, $options->get_keys()
+ if $config->{'help'};
+
+ my $msg = '';
+
+ if (@params) {
+ for ( grep !/^V_ALIAS/, @params ) {
+ if ( ref $_ eq 'ARRAY' ) {
+ $msg .= join "\n", @$_;
+ $msg .= "\n";
+ }
+ else {
+ $msg .= "$_\n";
+ }
+ }
+ }
+ else {
+ $msg .= "\n";
+ }
+
+ $self->{logglevel} = 'info';
+ $self->_display($msg);
+
+ exit 0 unless $INTERACTIVE;
+}
+
+sub warning {
+ my ( $self, $msg ) = @_;
+
+ my $str = "! $msg\n";
+
+ print STDERR color 'red' if $COLORFUL;
+ $self->_display( $str, *STDERR );
+ print STDERR color 'reset' if $COLORFUL;
+
+ return undef;
+}
+
+sub verbose {
+ my ( $self, @msgs ) = @_;
+
+ print STDERR color 'cyan' if $COLORFUL;
+ $self->{logglevel} = 'info';
+
+ if ( $self->is_verbose() ) {
+ for my $msg (@msgs) {
+ if ( $self->is_debug() ) {
+ my @caller = caller;
+ $self->_display("@caller: $msg\n");
+ }
+ else {
+ $self->_display("$msg\n");
+ }
+ }
+ }
+
+ print STDERR color 'reset' if $COLORFUL;
+
+ return undef;
+}
+
+sub dump {
+ my ( $self, $msg ) = @_;
+
+ $self->{logglevel} = 'warning';
+ $self->_display( Dumper $msg );
+
+ return undef;
+}
+
+sub debug {
+ my ( $self, @msgs ) = @_;
+
+ my @caller = caller;
+
+ if ( $self->is_debug() ) {
+ for my $msg (@msgs) {
+ $msg = Dumper $msg if ref $msg ne '';
+
+ my $str = "@caller: $msg\n";
+
+ $self->{logglevel} = 'debug';
+ $self->_display($str);
+ }
+ }
+
+ return undef;
+}
+
+1;
+
diff --git a/lib/MON/Filter.pm b/lib/MON/Filter.pm
new file mode 100644
index 0000000..d16d1c5
--- /dev/null
+++ b/lib/MON/Filter.pm
@@ -0,0 +1,166 @@
+package MON::Filter;
+
+use strict;
+use warnings;
+use v5.10;
+use autodie;
+
+use Data::Dumper;
+
+use MON::Display;
+use MON::Config;
+use MON::Utils;
+
+our @ISA = ('MON::Display');
+
+sub new {
+ my ( $class, %opts ) = @_;
+
+ my $self = bless \%opts, $class;
+
+ $self->init();
+
+ return $self;
+}
+
+sub init {
+ my ($self) = @_;
+
+ $self->{query_string} = '';
+ $self->{filters} = {};
+ $self->{num_filters} = 0;
+ $self->{is_computed} = 0;
+ $self->{or} = [];
+
+ return undef;
+}
+
+# Create filters with params
+sub compute {
+ my ( $self, $params ) = @_;
+
+ $self->debug( 'Computing filter using', $params );
+ return undef if $self->{is_computed};
+
+ my %likes;
+
+ if ( defined $params and ref $params eq 'ARRAY' ) {
+ while (@$params) {
+ my $op_token = pop @$params;
+ given ($op_token) {
+ when (/^OP_LIKE$/) {
+ my $arg2 = pop @$params;
+ my $arg1 = pop @$params;
+
+ if ( exists $likes{$arg1} ) {
+ $self->error(
+"Can not run multiple 'like's on '$arg1', since it is used for the API query_string"
+ );
+ }
+ else {
+ $likes{$arg1} = "$arg1=$arg2";
+ }
+
+ }
+ when (/^OP_/) {
+ $self->{filters}{$_} = [] unless exists $self->{filters}{$_};
+ my $arg2 = pop @$params;
+ my $arg1 = pop @$params;
+ push @{ $self->{filters}{$_} }, [ $arg1, $arg2 ];
+ $self->{num_filters}++;
+ }
+ default {
+ $self->error("Inernal error: Operator expected instead of $_");
+ }
+ }
+ }
+ }
+
+ $self->{query_string} = '?' . join( '&', values %likes );
+ $self->{is_computed} = 1;
+
+ $self->debug( 'Computed filter:', $self->{filters} );
+ $self->verbose( "Computed query string is: " . $self->{query_string} );
+
+ return undef;
+}
+
+sub filter {
+ my ( $self, $objects ) = @_;
+
+ my $config = $self->{config};
+ my $json = $self->{json};
+
+ return $objects unless $self->{num_filters};
+
+ my $num = sub {
+ my $str = shift;
+ $str =~ s/\D//g;
+ $str = 0 if $str eq '';
+ return int $str;
+ };
+
+ while ( my ( $op, $vals ) = each %{ $self->{filters} } ) {
+ for my $val (@$vals) {
+ my ( $key, $val ) = @$val;
+
+ @$objects = grep {
+ my $object = $_;
+
+ if ( exists $object->{$key} ) {
+ if ( $op eq 'OP_MATCHES' and $object->{$key} =~ /$val/ ) {
+ 1;
+
+ }
+ elsif ( $op eq 'OP_NMATCHES' and $object->{$key} !~ /$val/ ) {
+ 1;
+
+ }
+ elsif ( $op eq 'OP_EQ' and $object->{$key} eq $val ) {
+ 1;
+
+ }
+ elsif ( $op eq 'OP_NE' and $object->{$key} ne $val ) {
+ 1;
+
+ }
+ elsif ( $op eq 'OP_LT'
+ and $num->( $object->{$key} ) < $num->($val) )
+ {
+ 1;
+
+ }
+ elsif ( $op eq 'OP_LE'
+ and $num->( $object->{$key} ) <= $num->($val) )
+ {
+ 1;
+
+ }
+ elsif ( $op eq 'OP_GT'
+ and $num->( $object->{$key} ) > $num->($val) )
+ {
+ 1;
+
+ }
+ elsif ( $op eq 'OP_GE'
+ and $num->( $object->{$key} ) >= $num->($val) )
+ {
+ 1;
+
+ }
+ else {
+ 0;
+ }
+ }
+ else {
+ 0;
+ }
+ } @$objects;
+ }
+ }
+
+ return $objects;
+}
+
+1;
+
diff --git a/lib/MON/JSON.pm b/lib/MON/JSON.pm
new file mode 100644
index 0000000..e12b1ce
--- /dev/null
+++ b/lib/MON/JSON.pm
@@ -0,0 +1,51 @@
+package MON::JSON;
+
+use strict;
+use warnings;
+use v5.10;
+use autodie;
+
+use JSON;
+
+use MON::Display;
+use MON::Utils;
+
+our @ISA = ('MON::Display');
+
+our $JSON_XS = JSON::XS->new();
+
+sub new {
+ my ( $class, %opts ) = @_;
+
+ my $self = bless \%opts, $class;
+
+ $self->init();
+
+ return $self;
+}
+
+sub init {
+ my ($self) = @_;
+
+ return undef;
+}
+
+sub decode {
+ my ( $self, $json ) = @_;
+
+ return $JSON_XS->allow_nonref()->decode($json);
+}
+
+sub encode {
+ my ( $self, $vals ) = @_;
+
+ return $JSON_XS->pretty()->encode($vals);
+}
+
+sub encode_canonical {
+ my ( $self, $vals ) = @_;
+
+ return $JSON_XS->canonical()->pretty()->encode($vals);
+}
+
+1;
diff --git a/lib/MON/Options.pm b/lib/MON/Options.pm
new file mode 100644
index 0000000..b798f56
--- /dev/null
+++ b/lib/MON/Options.pm
@@ -0,0 +1,163 @@
+package MON::Options;
+
+use strict;
+use warnings;
+use v5.10;
+use autodie;
+
+use Data::Dumper;
+use Scalar::Util qw(looks_like_number);
+
+use MON::Utils;
+
+sub new {
+ my ( $class, %opts ) = @_;
+
+ my $self = bless \%opts, $class;
+
+ $self->init();
+ $self->parse();
+
+ return $self;
+}
+
+sub init {
+ my ($self) = @_;
+
+ my %opts = (
+ opts => {
+ config => '',
+ debug => 0,
+ dry => 0,
+ help => 0,
+ interactive => 0,
+ meta => 0,
+ nocolor => 0,
+ quiet => 0,
+ syslog => 0,
+ unique => 0,
+ verbose => 0,
+ version => 0,
+ errfile => '',
+ },
+ opts_short => {
+ c => 'config',
+ D => 'debug',
+ d => 'dry',
+ i => 'interactive',
+ h => 'help',
+ m => 'meta',
+ n => 'nocolor',
+ q => 'quiet',
+ s => 'syslog',
+ u => 'unique',
+ v => 'verbose',
+ V => 'version',
+ R => 'errfile',
+ },
+ unknown => [],
+ );
+
+ $self->{$_} = $opts{$_} for keys %opts;
+
+ return undef;
+}
+
+sub parse {
+ my ($self) = @_;
+
+ my $opts_passed = $self->{opts_passed};
+
+ for my $opt (@$opts_passed) {
+ my ( $k, $v ) = split /=/, $opt;
+
+ # Longopt
+ if ( $k =~ s/^--// && isin $k, keys %{ $self->{opts} } ) {
+ if ( defined $v ) {
+ $self->{opts}{$k} = $v;
+ }
+ else {
+ $self->{opts}{$k} = 1;
+ }
+ }
+
+ # Shortopt
+ elsif ( $k =~ s/^-// && isin $k, keys %{ $self->{opts_short} } ) {
+ if ( defined $v ) {
+ $self->{opts}{ $self->{opts_short}{$k} } = $v;
+ }
+ else {
+ $self->{opts}{ $self->{opts_short}{$k} } = 1;
+ }
+
+ }
+ elsif ( $k !~ /\./ ) {
+
+ # If key is not separated by dot, it is unknown
+ push @{ $self->{unknown} }, $opt;
+
+ }
+ else {
+
+ # Otherise it might overwrite a value of mon.conf
+ $self->{opts}{$k} = $v;
+ }
+ }
+
+ # Help implies dry mode
+ $self->{opts}{dry} = 1 if $self->{opts}{help};
+
+ # Debug implies verbose mode
+ $self->{opts}{verbose} = 1 if $self->{opts}{debug};
+
+ return undef;
+}
+
+sub get_keys {
+ my ($self) = @_;
+ my @keys;
+
+ while ( my ( $k, $v ) = each %{ $self->{opts_short} } ) {
+ if ( looks_like_number( $self->{opts}{$v} ) ) {
+ push @keys, "--$v -$k";
+ }
+ else {
+ push @keys, "--$v=VAL -$k=VAL";
+ }
+ }
+
+ return @keys;
+}
+
+sub store {
+ my ( $self, $config ) = @_;
+
+ $self->store_first($config);
+ $self->store_after($config);
+
+ return undef;
+}
+
+# Only store values which are not separated by dots
+sub store_first {
+ my ( $self, $config ) = @_;
+
+ for ( grep !/\./, keys %{ $self->{opts} } ) {
+ $config->{$_} = $self->{opts}{$_};
+ }
+
+ return undef;
+}
+
+# Only store values which are separated by dots
+sub store_after {
+ my ( $self, $config ) = @_;
+
+ for ( grep /\./, keys %{ $self->{opts} } ) {
+ $config->{$_} = $self->{opts}{$_};
+ }
+
+ return undef;
+}
+
+1;
diff --git a/lib/MON/Query.pm b/lib/MON/Query.pm
new file mode 100644
index 0000000..d7e223b
--- /dev/null
+++ b/lib/MON/Query.pm
@@ -0,0 +1,557 @@
+package MON::Query;
+
+use strict;
+use warnings;
+use v5.10;
+
+use Data::Dumper;
+
+use MON::Display;
+use MON::Config;
+use MON::Utils;
+use MON::QueryBase;
+
+our @ISA = ('MON::QueryBase');
+
+sub new {
+ my ( $class, %opts ) = @_;
+
+ my $self = bless \%opts, $class;
+
+ $self->init();
+
+ return $self;
+}
+
+sub init {
+ my ($self) = @_;
+
+ $self->{querystack} = [];
+ $self->{args} = [ map { s/^V_/:V_/; $_ } @{ $self->{args} } ];
+
+ return undef;
+}
+
+sub tree {
+ my ($self) = @_;
+
+ my $api = $self->{api};
+ my $paths = $api->get_possible_paths();
+
+ my ( $s, $r ) = ( $self, $api );
+
+ my $arr = sub {
+ my ( $keys, $vals ) = @_;
+ map { $_ => shift @$vals } @$keys;
+ };
+
+# _ => By default to run anonymous sub if no other key is specified in command line
+# __ => Always to run anonymous sub in the beginning of the current recursion
+# ___ => Always to run anonymous sub before next recursion
+# __DO => Process recursion right away, only do __ if exists
+# V_FOO => Declare variable FOO
+
+ my $where = {
+ _ => sub {
+ my $d = shift;
+ $s->possible( $r->get_path_params( $d->{V_PATH} ) );
+ },
+ V_KEY => {
+ __ => sub {
+ my $d = shift;
+ $s->check_has( $d->{V_KEY}, $r->get_path_params( $d->{V_PATH} ) );
+ },
+ like => {
+ V_VAL => {
+ _ => sub {
+ my $d = shift;
+ my $path = $d->{V_PATH};
+ $s->out_json( $d->{where_action}($path) );
+ },
+ },
+ ___ => sub { $s->push_querystack('OP_LIKE') }
+ },
+ matches => {
+ V_VAL => {
+ _ => sub {
+ my $d = shift;
+ my $path = $d->{V_PATH};
+ $s->out_json( $d->{where_action}($path) );
+ },
+ },
+ ___ => sub { $s->push_querystack('OP_MATCHES') }
+ },
+ nmatches => {
+ V_VAL => {
+ _ => sub {
+ my $d = shift;
+ my $path = $d->{V_PATH};
+ $s->out_json( $d->{where_action}($path) );
+ },
+ },
+ ___ => sub { $s->push_querystack('OP_NMATCHES') }
+ },
+ eq => {
+ V_VAL => {
+ _ => sub {
+ my $d = shift;
+ my $path = $d->{V_PATH};
+ $s->out_json( $d->{where_action}($path) );
+ },
+ },
+ ___ => sub { $s->push_querystack('OP_EQ') }
+ },
+ ne => {
+ V_VAL => {
+ _ => sub {
+ my $d = shift;
+ my $path = $d->{V_PATH};
+ $s->out_json( $d->{where_action}($path) );
+ },
+ },
+ ___ => sub { $s->push_querystack('OP_NE') }
+ },
+ lt => {
+ V_VAL => {
+ _ => sub {
+ my $d = shift;
+ my $path = $d->{V_PATH};
+ $s->out_json( $d->{where_action}($path) );
+ },
+ },
+ ___ => sub { $s->push_querystack('OP_LT') }
+ },
+ le => {
+ V_VAL => {
+ _ => sub {
+ my $d = shift;
+ my $path = $d->{V_PATH};
+ $s->out_json( $d->{where_action}($path) );
+ },
+ },
+ ___ => sub { $s->push_querystack('OP_LE') }
+ },
+ gt => {
+ V_VAL => {
+ _ => sub {
+ my $d = shift;
+ my $path = $d->{V_PATH};
+ $s->out_json( $d->{where_action}($path) );
+ },
+ },
+ ___ => sub { $s->push_querystack('OP_GT') }
+ },
+ ge => {
+ V_VAL => {
+ _ => sub {
+ my $d = shift;
+ my $path = $d->{V_PATH};
+ $s->out_json( $d->{where_action}($path) );
+ },
+ },
+ ___ => sub { $s->push_querystack('OP_GE') }
+ },
+ },
+ };
+
+ for my $op ( sort qw(like matches nmatches eq ne lt le gt ge) ) {
+ $where->{V_KEY}{$op}{V_VAL}{and}{__DO} = $where;
+ $where->{V_KEY}{$op}{V_VAL}{'V_ALIAS:a'} = $where->{V_KEY}{$op}{V_VAL}{and};
+ }
+
+ $where->{V_KEY}{'V_ALIAS:l'} = $where->{V_KEY}{like};
+ $where->{V_KEY}{'V_ALIAS:~'} = $where->{V_KEY}{like};
+ $where->{V_KEY}{'V_ALIAS:=='} = $where->{V_KEY}{eq};
+ $where->{V_KEY}{'V_ALIAS:!='} = $where->{V_KEY}{ne};
+ $where->{V_KEY}{'V_ALIAS:=~'} = $where->{V_KEY}{matches};
+ $where->{V_KEY}{'V_ALIAS:!~'} = $where->{V_KEY}{nmatches};
+
+ my $set_where = {
+ _ => sub {
+ my $d = shift;
+ $s->possible( $r->get_path_params( $d->{V_PATH} ) );
+ },
+ V_SETKEY => {
+ '=' => {
+ V_SETVAL => {
+ __ => sub {
+ my $d = shift;
+ $d->{where_action} = $d->{set_action};
+ },
+ where => $where,
+ },
+ },
+ },
+ };
+ $set_where->{V_SETKEY}{'='}{V_SETVAL}{and} = $set_where;
+ $set_where->{V_SETKEY}{'='}{V_SETVAL}{'V_ALIAS::'} =
+ $set_where->{V_SETKEY}{'='}{V_SETVAL}{where};
+ $set_where->{V_SETKEY}{'='}{V_SETVAL}{'V_ALIAS:a'} =
+ $set_where->{V_SETKEY}{'='}{V_SETVAL}{and};
+
+ my $set = {
+ _ => sub {
+ my $d = shift;
+ $s->possible( $r->get_path_params( $d->{V_PATH} ) );
+ },
+ V_SETKEY => {
+ '=' => {
+ V_SETVAL => {
+ _ => sub {
+ my $d = shift;
+ my $path = $d->{V_PATH};
+
+ $s->out_json( $d->{set_action}($path) );
+ },
+ },
+ },
+ },
+ };
+ $set->{V_SETKEY}{'='}{V_SETVAL}{and} = $set;
+ $set->{V_SETKEY}{'='}{V_SETVAL}{'V_ALIAS:a'} =
+ $set->{V_SETKEY}{'='}{V_SETVAL}{and};
+
+ my $remove = {
+ _ => sub {
+ my $d = shift;
+ $s->possible( $r->get_path_params( $d->{V_PATH} ) );
+ },
+ V_REMOVEKEY => {
+ __ => sub {
+ my $d = shift;
+ $d->{where_action} = $d->{remove_action};
+ },
+ where => $where,
+ },
+ };
+ $remove->{V_REMOVEKEY}{and} = $remove;
+ $remove->{V_REMOVEKEY}{'V_ALIAS:a'} = $remove->{V_REMOVEKEY}{and};
+
+ my $tree = {
+ get => {
+ _ => sub { $s->possible(@$paths) },
+ V_PATH => {
+ __ => sub {
+ my $d = shift;
+ $s->check_has( $d->{V_PATH}, $paths );
+ $d->{where_action} = sub {
+ my ($path) = @_;
+ $r->fetch_path_json( $path, $s->get_querystack() );
+ };
+ },
+ _ => sub {
+ my $d = shift;
+ $s->out_json(
+ $r->fetch_path_json( $d->{V_PATH}, $s->get_querystack() ) );
+ },
+ where => $where,
+ },
+ },
+ getfmt => {
+ V_FORMAT => {
+ _ => sub { $s->possible(@$paths) },
+ V_PATH => {
+ __ => sub {
+ my $d = shift;
+ $s->check_has( $d->{V_PATH}, $paths );
+ $d->{where_action} = sub {
+ my ($path) = @_;
+ $s->out_format( $d->{V_FORMAT},
+ $r->fetch_path_json( $path, $s->get_querystack() ) );
+ };
+ },
+ _ => sub {
+ my $d = shift;
+ $s->out_format( $d->{V_FORMAT},
+ $r->fetch_path_json( $d->{V_PATH}, $s->get_querystack() ) );
+ },
+ where => $where,
+ },
+ },
+ },
+ edit => {
+ _ => sub { $s->possible(@$paths) },
+ V_PATH => {
+ __ => sub {
+ my $d = shift;
+ $s->check_has( $d->{V_PATH}, $paths );
+ $d->{where_action} = sub {
+ my ($path) = @_;
+ $s->edit_path_data( $path,
+ $r->fetch_path_json( $path, $s->get_querystack() ) );
+ };
+ },
+ _ => sub {
+ my $d = shift;
+ $s->edit_path_data( $d->{V_PATH},
+ $r->fetch_path_json( $d->{V_PATH} ) );
+ },
+ where => $where,
+ },
+ },
+ view => {
+ _ => sub { $s->possible(@$paths) },
+ V_PATH => {
+ __ => sub {
+ my $d = shift;
+ $s->check_has( $d->{V_PATH}, $paths );
+ $d->{where_action} = sub {
+ my ($path) = @_;
+ $s->view_data( $path,
+ $r->fetch_path_json( $path, $s->get_querystack() ) );
+ };
+ },
+ _ => sub {
+ my $d = shift;
+ $s->view_data( $d->{V_PATH}, $r->fetch_path_json( $d->{V_PATH} ) );
+ },
+ where => $where,
+ },
+ },
+ delete => {
+ _ => sub { $s->possible(@$paths) },
+ V_PATH => {
+ __ => sub {
+ my $d = shift;
+ $s->check_has( $d->{V_PATH}, $paths );
+ $d->{where_action} = sub {
+ my ($path) = @_;
+ $s->out_json( $r->delete_path_json( $path, $s->get_querystack() ) );
+ };
+ },
+ _ => sub {
+ my $d = shift;
+ $s->out_json( $r->fetch_path_json( $d->{V_PATH} ) );
+ },
+ where => $where,
+ },
+ },
+ update => {
+ _ => sub { $s->possible(@$paths) },
+ V_PATH => {
+ __ => sub {
+ my $d = shift;
+ $s->check_has( $d->{V_PATH}, $paths );
+ $d->{set_action} = sub {
+ my ($path) = @_;
+ my %set = $arr->( $d->{ALL_V_SETKEY}, $d->{ALL_V_SETVAL} );
+ $s->out_json(
+ $r->update_path_json( $path, $s->get_querystack(), \%set ) );
+ };
+ $d->{remove_action} = sub {
+ my ($path) = @_;
+ my $remove = $d->{ALL_V_REMOVEKEY};
+ $s->out_json(
+ $r->update_remove_path_json(
+ $path, $s->get_querystack(), $remove
+ )
+ );
+ };
+ },
+ set => $set_where,
+ 'delete' => $remove,
+ },
+ },
+ insert => {
+ _ => sub { $s->possible(@$paths) },
+ V_PATH => {
+ __ => sub {
+ my $d = shift;
+ $s->check_has( $d->{V_PATH}, $paths );
+ $d->{set_action} = sub {
+ my ($path) = @_;
+ my %set = $arr->( $d->{ALL_V_SETKEY}, $d->{ALL_V_SETVAL} );
+ $s->out_json( $self->insert_data( $path, \%set ) );
+ };
+ },
+ set => $set,
+ },
+ },
+ post => {
+ _ => sub { $s->possible(@$paths) },
+ V_PATH => {
+ __ => sub {
+ my $d = shift;
+ $s->check_has( $d->{V_PATH}, $paths );
+ },
+ _ => sub {
+ my $d = shift;
+ $s->send_data( $d->{V_PATH}, 'POST' );
+ },
+ from => {
+ V_FILEPATH => {
+ _ => sub {
+ my $d = shift;
+ $s->send_data( $d->{V_PATH}, 'POST', $d->{V_FILEPATH} );
+ }
+ }
+ }
+ },
+ },
+ put => {
+ _ => sub { $s->possible(@$paths) },
+ V_PATH => {
+ __ => sub {
+ my $d = shift;
+ $s->check_has( $d->{V_PATH}, $paths );
+ },
+ _ => sub {
+ my $d = shift;
+ $s->send_data( $d->{V_PATH}, 'PUT' );
+ },
+ from => {
+ V_FILEPATH => {
+ _ => sub {
+ my $d = shift;
+ $s->send_data( $d->{V_PATH}, 'PUT', $d->{V_FILEPATH} );
+ }
+ }
+ }
+ },
+ },
+ verify => sub { $s->verify() },
+ restart => sub { $s->restart() },
+ reload => sub { $s->restart() },
+ 'V_ALIAS:y' => sub { $s->verify() },
+ 'V_ALIAS:r' => sub { $s->restart() },
+ };
+
+ $tree->{delete}{V_PATH}{'V_ALIAS::'} = $tree->{delete}{V_PATH}{where};
+ $tree->{'V_ALIAS:d'} = $tree->{delete};
+
+ $tree->{edit}{V_PATH}{'V_ALIAS::'} = $tree->{edit}{V_PATH}{where};
+ $tree->{'V_ALIAS:e'} = $tree->{edit};
+
+ $tree->{get}{V_PATH}{'V_ALIAS::'} = $tree->{get}{V_PATH}{where};
+ $tree->{'V_ALIAS:g'} = $tree->{get};
+
+ $tree->{getfmt}{V_FORMAT}{V_PATH}{'V_ALIAS::'} =
+ $tree->{getfmt}{V_FORMAT}{V_PATH}{where};
+ $tree->{'V_ALIAS:f'} = $tree->{getfmt};
+
+ $tree->{insert}{V_PATH}{'V_ALIAS:s'} = $tree->{insert}{V_PATH}{set};
+ $tree->{'V_ALIAS:i'} = $tree->{insert};
+
+ $tree->{'V_ALIAS:p'} = $tree->{post};
+
+ $tree->{'V_ALIAS:t'} = $tree->{put};
+
+ $tree->{update}{V_PATH}{'V_ALIAS:d'} = $tree->{update}{V_PATH}{delete};
+ $tree->{update}{V_PATH}{'V_ALIAS:s'} = $tree->{update}{V_PATH}{set};
+ $tree->{'V_ALIAS:u'} = $tree->{update};
+
+ $tree->{view}{V_PATH}{'V_ALIAS::'} = $tree->{view}{V_PATH}{where};
+ $tree->{'V_ALIAS:v'} = $tree->{view};
+
+ $self->debug( 'Abstract syntax tree:', $tree );
+
+ return $tree;
+}
+
+sub parse {
+ my ($self) = @_;
+
+ my $config = $self->{config};
+ my $args = $self->{args};
+
+ # Get > and < operators (only needed by interactive mode)
+ $config->{outfile} = $config->{infile} = undef;
+ if ( defined $args->[-2] ) {
+ given ( $args->[-2] ) {
+ when ('>') {
+ my ( undef, $file ) = splice @$args, -2, 2;
+ open $config->{outfile}, '>', $file or $self->warning("$file: $!");
+ }
+ when ('<') {
+ my ( undef, $file ) = splice @$args, -2, 2;
+ open $config->{infile}, '<', $file or $self->warning("$file: $!");
+ }
+ }
+ }
+
+ my $ret = $self->traverse( $args, $self->tree(), {} );
+
+ close $config->{infile} if defined $config->{infile};
+ close $config->{outfile} if defined $config->{outfile};
+
+ return $ret;
+}
+
+sub traverse {
+ my ( $self, $args, $tree, $data ) = @_;
+
+ $self->debug( 'Traversing args: ' . Dumper $args);
+ $self->debug( 'Traversing data: ' . Dumper $data);
+
+ if ( ref $tree eq 'CODE' ) {
+ $tree->($data);
+ return undef;
+ }
+
+ $tree->{__}->($data) if exists $tree->{__};
+
+ if ( exists $tree->{__DO} ) {
+ $self->traverse( $args, $tree->{__DO}, $data );
+ return undef;
+ }
+
+ my @possible = grep !/^__?$/, sort keys %$tree;
+ my $token = $possible[0];
+
+ unless (@$args) {
+ if ( exists $tree->{_} ) {
+ $tree->{_}->($data);
+ }
+ else {
+ $self->possible(@possible);
+ }
+ }
+ else {
+ my $arg = shift @$args;
+
+ if ( exists $tree->{$arg} ) {
+ $tree->{___}->($data) if exists $tree->{___};
+ $self->traverse( $args, $tree->{$arg}, $data );
+ }
+ elsif ( exists $tree->{"V_ALIAS:$arg"} ) {
+ $tree->{___}->($data) if exists $tree->{___};
+ $self->traverse( $args, $tree->{"V_ALIAS:$arg"}, $data );
+ }
+ elsif ( defined $token && $token =~ /^V_/ && $token !~ /^V_ALIAS:/ ) {
+ $data->{$token} = $arg;
+ $self->push_querystack($arg) if $token =~ /^V_(?:KEY|VAL)/;
+
+ unless ( exists $data->{"ALL_$token"} ) {
+ $data->{"ALL_$token"} = [$arg];
+ }
+ else {
+ push @{ $data->{"ALL_$token"} }, $arg;
+ }
+
+ $tree->{___}->($data) if exists $tree->{___};
+ $self->traverse( $args, $tree->{$token}, $data );
+ }
+ else {
+ $self->error("'$arg' unexpected here");
+ }
+ }
+
+ return undef;
+}
+
+sub push_querystack {
+ my ( $self, $token ) = @_;
+
+ $self->debug("Pushing token '$token' to querystack");
+ push @{ $self->{querystack} }, $token;
+
+ return undef;
+}
+
+sub get_querystack {
+ my ($self) = @_;
+
+ return $self->{querystack};
+}
+
+1;
diff --git a/lib/MON/QueryBase.pm b/lib/MON/QueryBase.pm
new file mode 100644
index 0000000..6ddfb99
--- /dev/null
+++ b/lib/MON/QueryBase.pm
@@ -0,0 +1,232 @@
+package MON::QueryBase;
+
+use strict;
+use warnings;
+use v5.10;
+
+use File::Temp qw/:mktemp/;
+use Data::Dumper;
+use Digest::SHA;
+
+use MON::Display;
+use MON::Config;
+use MON::Utils;
+
+our @ISA = ('MON::Display');
+
+sub check_has {
+ my ( $self, $key, $in ) = @_;
+
+ if ( ref $in eq 'HASH' && exists $in->{$key} ) {
+ return 1;
+
+ }
+ else {
+ for (@$in) {
+ return 1 if $_ eq $key;
+ }
+ }
+
+ my @possible = sort ( ref $in eq 'HASH' ? keys %$in : @$in );
+ $self->error("'$key' not expected here. Possible: @possible");
+}
+
+sub edit_path_file_send {
+ my ( $self, $path, $filename ) = @_;
+
+ my $api = $self->{api};
+
+ open my $fh, $filename or die "$filename: $!";
+ my @data = <$fh>;
+ close $fh;
+
+ $self->info("Saving data to API into $path from file $filename");
+ $self->out_json(
+ $api->send_path_json( $path, join( '', @data ), undef, 'PUT' ) );
+
+ return undef;
+}
+
+sub get_sha_of_file {
+ my ( $self, $filename ) = @_;
+
+ my $sha = Digest::SHA->new();
+ open my $sha_fh, $filename or die "$!\n";
+ $sha->addfile($sha_fh);
+ $sha = $sha->b64digest();
+ close $sha_fh;
+
+ return $sha;
+}
+
+sub edit_path_file {
+ my ( $self, $path, $filename ) = @_;
+
+ my $config = $self->{config};
+ my $api = $self->{api};
+
+ if ( $config->{'dry'} ) {
+ $self->verbose("Dry mode, don't modify anything via API.");
+ return undef;
+ }
+
+ my $editor = $ENV{EDITOR} // 'vim';
+
+ my $sha_before = $self->get_sha_of_file($filename);
+ $self->verbose("Checksum of $filename before edit: $sha_before");
+
+ for ( ; ; ) {
+ system("$editor $filename");
+ my $sha_after = $self->get_sha_of_file($filename);
+ $self->verbose("Checksum of $filename after edit: $sha_after");
+
+ if ( $sha_before eq $sha_after ) {
+ $self->info(
+ "Dude, no changes were made. I am not sending data back to the API!");
+ last;
+ }
+
+ $self->edit_path_file_send( $path, $filename );
+ if ( $api->{has_error} ) {
+ $self->info('An error has occured, press any key to re-edit');
+ <STDIN>;
+ }
+ else {
+ last;
+ }
+ }
+
+ for ( glob("/tmp/mon*.json") ) {
+ $self->verbose("Cleaning up tempfile $_");
+ unlink $_;
+ }
+
+ return undef;
+}
+
+sub edit_path_data {
+ my ( $self, $path, $data ) = @_;
+
+ my $config = $self->{config};
+ my $api = $self->{api};
+ my $json = $api->{json};
+
+ my ( $fh, $filename ) = mkstemps( "/tmp/monXXXXXX", '.json' );
+
+ # Sort the json
+ my $vals = $json->decode($data);
+ print $fh $json->encode_canonical($vals);
+ close $fh;
+
+ $self->edit_path_file( $path, $filename );
+
+ return undef;
+}
+
+sub view_data {
+ my ( $self, $path, $data ) = @_;
+
+ my $config = $self->{config};
+ my $api = $self->{api};
+ my $json = $api->{json};
+
+ if ( $config->{'dry'} ) {
+ $self->verbose("Dry mode, don't modify anything via API.");
+ return undef;
+ }
+ my ( $fh, $filename ) = mkstemps( "/tmp/monXXXXXX", '.json' );
+
+ # Sort the json
+ my $vals = $json->decode($data);
+ print $fh $json->encode_canonical($vals);
+ close $fh;
+
+ my $editor = $ENV{PAGER} // 'view';
+ system("$editor $filename");
+
+ unlink $filename;
+ return undef;
+}
+
+sub insert_data {
+ my ( $self, $path, $set ) = @_;
+
+ my $config = $self->{config};
+ my $api = $self->{api};
+
+ if ( $config->{'dry'} ) {
+ $self->verbose("Dry mode, don't modify anything via API.");
+ return undef;
+ }
+
+ return $api->send_path_json( $path, $api->{json}->encode($set) );
+}
+
+sub send_data {
+ my ( $self, $path, $method, $fromfile ) = @_;
+
+ my $config = $self->{config};
+ my $api = $self->{api};
+ my @send_data;
+
+ if ( defined $config->{infile} ) {
+ my $infile = $config->{infile};
+
+ # Slurp it, it's not gonna be >1mb anyway
+ @send_data = <$infile>;
+ }
+ elsif ( defined $fromfile ) {
+ open my $fh, $fromfile or do {
+ $self->error("Can not open file $fromfile: $!");
+ return undef;
+ };
+
+ # Slurp it, it's not gonna be >1mb anyway
+ @send_data = <$fh>;
+ close $fh;
+ }
+ else {
+
+ # Slurp it, it's not gonna be >1mb anyway
+ @send_data = <STDIN>;
+ }
+
+ unless (@send_data) {
+ $self->error(
+"No post data found. Use 'from datafile' or pipes to set post or put data."
+ );
+ return undef;
+ }
+
+ my $send_data = join '', @send_data;
+
+ my $json = $api->{json}->decode($send_data);
+
+ if ( ref $json eq 'ARRAY' && @$json && ref $json->[0] ne 'HASH' ) {
+ $self->verbose('Transforming array style JSON into an hash style one');
+ my %json = @$json;
+ $json = \%json;
+ }
+
+ $self->out_json(
+ $api->send_path_json( $path, $api->{json}->encode($json), undef, $method )
+ );
+
+ return undef;
+}
+
+sub verify {
+ my ($self) = @_;
+ my $api = $self->{api};
+
+ $self->out_json( $api->post_verify_json() );
+}
+
+sub restart {
+ my ($self) = @_;
+ my $api = $self->{api};
+
+ $self->out_json( $api->post_restart_json() );
+}
+
+1;
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;
diff --git a/lib/MON/Syslogger.pm b/lib/MON/Syslogger.pm
new file mode 100644
index 0000000..9292085
--- /dev/null
+++ b/lib/MON/Syslogger.pm
@@ -0,0 +1,77 @@
+package MON::Syslogger;
+
+use strict;
+use warnings;
+use v5.10;
+use autodie;
+
+use Unix::Syslog qw(:macros :subs);
+use Scalar::Util qw(looks_like_number);
+
+sub new {
+ my ( $class, %opts ) = @_;
+
+ my $self = bless \%opts, $class;
+
+ $self->init();
+
+ return $self;
+}
+
+sub init {
+ my ($self) = @_;
+
+ my $options = $self->{options};
+ $options->store($self);
+
+ if ( exists $self->{syslog} && $self->{syslog} ne '0' ) {
+ $self->{enable} = 1;
+
+ }
+ elsif ( exists $ENV{MON_SYSLOG} && $ENV{MON_SYSLOG} ne '0' ) {
+ $self->{enable} = 1;
+
+ }
+ else {
+ $self->{enable} = 0;
+ }
+
+ return undef;
+}
+
+sub logg {
+ my ( $self, $level, @msgs ) = @_;
+
+ return undef unless $self->{enable};
+
+ openlog $0, LOG_PID, LOG_LOCAL0;
+
+ s/\n/ /g for @msgs;
+
+ given ($level) {
+ when ('debug') {
+ syslog LOG_DEBUG, $_ for @msgs;
+ }
+ when ('warning') {
+ syslog LOG_WARNING, $_ for @msgs;
+ }
+ when ('error') {
+ syslog LOG_ERR, $_ for @msgs;
+ }
+ when ('notice') {
+ syslog LOG_NOTICE, $_ for @msgs;
+ }
+ when ('info') {
+ syslog LOG_INFO, $_ for @msgs;
+ }
+ default {
+ $self->logg( 'info', @msgs )
+ }
+ }
+
+ closelog
+
+ return undef;
+}
+
+1;
diff --git a/lib/MON/Utils.pm b/lib/MON/Utils.pm
new file mode 100644
index 0000000..791af8e
--- /dev/null
+++ b/lib/MON/Utils.pm
@@ -0,0 +1,80 @@
+package MON::Utils;
+
+use strict;
+use warnings;
+use v5.10;
+use autodie;
+
+use Data::Dumper;
+use Exporter;
+
+use base 'Exporter';
+
+our @EXPORT = qw (
+ d
+ dumper
+ get_version
+ isin
+ newline
+ notnull
+ null
+ remove_spaces
+ say
+ sum
+ trim
+);
+
+sub say (@) { print "$_\n" for @_; return undef }
+sub newline () { say ''; return undef }
+sub sum (@) { my $sum = 0; $sum += $_ for @_; return $sum }
+sub null ($) { defined $_[0] ? $_[0] : 0 }
+sub notnull ($) { $_[0] != 0 ? $_[0] : 1 }
+sub dumper (@) { die Dumper @_ }
+sub d (@) { dumper @_ }
+
+sub isin ($@) {
+ my ( $elem, @list ) = @_;
+
+ for (@list) {
+ return 1 if $_ eq $elem;
+ }
+
+ return 0;
+}
+
+sub trim ($) {
+ my $trimit = shift;
+
+ $trimit =~ s/^[\s\t]+//;
+ $trimit =~ s/[\s\t]+$//;
+
+ return $trimit;
+}
+
+sub remove_spaces ($) {
+ my $str = shift;
+
+ $str =~ s/[\s\t]//g;
+
+ return $str;
+}
+
+sub get_version () {
+ my $versionfile = do {
+ if ( -f '.version' ) {
+ '.version';
+ }
+ else {
+ '/usr/share/mon/version';
+ }
+ };
+
+ open my $fh, $versionfile or error("$!: $versionfile");
+ my $version = <$fh>;
+ close $fh;
+
+ chomp $version;
+ return $version;
+}
+
+1;
diff --git a/mi b/mi
new file mode 100755
index 0000000..49efcc0
--- /dev/null
+++ b/mi
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+exec mon --meta --interactive $@
diff --git a/mon b/mon
new file mode 100755
index 0000000..162f71a
--- /dev/null
+++ b/mon
@@ -0,0 +1,133 @@
+#!/usr/bin/perl
+#
+# (c) 2013, 2014 1&1 Internet AG
+# Paul C. Buetow <paul@buetow.org>
+
+use strict;
+use warnings;
+use v5.10;
+use autodie;
+
+use Data::Dumper;
+use Term::ReadLine;
+
+$| = 1;
+my $lib;
+
+BEGIN {
+ if ( -d './lib/MON' ) {
+ $lib = './lib';
+
+ }
+ else {
+ $lib = '/usr/share/mon/lib';
+ }
+
+ unless ( exists $ENV{HTTPS_CA_FILE} ) {
+ if ( -f 'ca.pem' ) {
+ $ENV{HTTPS_CA_FILE} = 'ca.pem';
+ }
+ elsif ( -f '/etc/ssl/certs/ca.pem' ) {
+ $ENV{HTTPS_CA_FILE} = '/etc/ssl/certs/ca.pem';
+ }
+ else {
+ $ENV{HTTPS_CA_FILE} = '/usr/share/mon/ca.pem';
+ }
+ }
+}
+
+use lib $lib;
+
+use MON::RESTlos;
+use MON::Config;
+use MON::Options;
+use MON::Query;
+use MON::Syslogger;
+use MON::Utils;
+use MON::Display;
+
+sub main (@) {
+ my @args = @_;
+ my @opts;
+
+ # Only interpret OPTIONS if they are at the beginning
+ push @opts, shift @args while @args and $args[0] =~ /^-/;
+
+ # .. or at the end
+ push @opts, pop @args while @args and $args[-1] =~ /^-/;
+
+ my $options = MON::Options->new( opts_passed => \@opts );
+ my $logger = MON::Syslogger->new( options => $options );
+ my $config = MON::Config->new( options => $options, logger => $logger );
+
+ if ( $config->{'version'} ) {
+ print get_version(), "\n";
+ exit 0;
+ }
+
+ $logger->logg( 'info', "Invoked by $ENV{USER} (params: @ARGV)" );
+
+ if ( $config->{interactive} ) {
+ my $term = Term::ReadLine->new('Monitoring API tool');
+ my $prompt = '>> ';
+ my $out = $term->OUT || \*STDOUT;
+
+ say $out "Welcome to the Monitoring API Tool v" . get_version();
+ say $out "Press Ctrl+D to exit; Prefix cmd with ! to run via shell";
+
+ while ( defined( $_ = $term->readline($prompt) ) ) {
+ $term->addhistory($_) if /\S/;
+
+ if (s/^!//) {
+ system($_);
+ }
+ else {
+
+ my $line = $_;
+
+ my @args = split / +/, $line;
+
+ my $api = MON::RESTlos->new( config => $config );
+ my $query = MON::Query->new(
+ config => $config,
+ api => $api,
+ options => $options,
+ args => \@args
+ );
+ $query->parse();
+ }
+ }
+
+ }
+ else {
+ my $api = MON::RESTlos->new( config => $config );
+ my $query = MON::Query->new(
+ config => $config,
+ api => $api,
+ options => $options,
+ args => \@args
+ );
+
+ $query->parse();
+ $query->verbose("Good bye");
+
+ if ( $api->{had_error} != 0 ) {
+
+ # Needed by Puppet to re-try operation the next Puppet run
+ if ( $config->{errfile} ne '' ) {
+ open my $fh, $config->{errfile} or die "$!: $config->{errfile}";
+ print $fh "Exited with an error\n";
+ close $fh;
+ }
+
+ exit 2;
+ }
+ else {
+ unlink $config->{errfile}
+ if $config->{errfile} ne '' and -f $config->{errfile};
+ exit 0;
+ }
+ }
+}
+
+main @ARGV;
diff --git a/mon.conf b/mon.conf
new file mode 100644
index 0000000..3cb6460
--- /dev/null
+++ b/mon.conf
@@ -0,0 +1,23 @@
+# Mandatory
+restlos.api.host: mamat-monitoringapi.infra.server.lan
+
+# Optional, default value is 'https'
+restlos.api.protocol: https
+
+# Optional, default value is '443'
+restlos.api.port: 443
+
+# Optional, default value is $USER
+restlos.auth.username: USERNAME
+
+# Mandatory
+restlos.auth.password.enc: PASSWORDBASE64ENCODED
+
+# Optional, default value is '1'
+backups.disable: 1
+
+# Optional, default value is '7'
+backups.keep.days: 0.1
+
+# Optional, default value is '~/.mon'
+backups.dir: ~/.mon
diff --git a/redhat-specs-add.txt b/redhat-specs-add.txt
new file mode 100644
index 0000000..1241b2c
--- /dev/null
+++ b/redhat-specs-add.txt
@@ -0,0 +1,2 @@
+Requires: perl-Digest-SHA, perl-libwww-perl, perl-JSON, perl-JSON-XS, perl-Unix-Syslog, perl-IO-Socket-SSL, perl-Term-ReadLine-Gnu
+BuildArch: noarch
diff --git a/t/Makefile b/t/Makefile
new file mode 100644
index 0000000..84197b2
--- /dev/null
+++ b/t/Makefile
@@ -0,0 +1,2 @@
+all:
+ MON_CONFIG=~/.mon-test.conf prove .
diff --git a/t/delete.t b/t/delete.t
new file mode 100644
index 0000000..7b4a9e6
--- /dev/null
+++ b/t/delete.t
@@ -0,0 +1,28 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use v5.10;
+
+use JSON;
+use Test::More;
+
+chdir '..';
+my $json;
+
+$json = decode_json `./mon --nocolor insert host set use = generic-host and name = footest01.server.lan and host_name = footest01.server.lan and address = 127.0.0.1`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing insert');
+
+$json = decode_json `./mon --nocolor insert host set use = generic-host and name = footest02.server.lan and host_name = footest02.server.lan and address = 127.0.0.1`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing insert');
+
+$json = decode_json `./mon --nocolor delete host where name like 'footest01.server.lan'`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing delete');
+
+$json = decode_json `./mon --nocolor delete host where name like 'footest02.server.lan'`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing delete');
+
+$json = decode_json `./mon --nocolor get host where name matches 'footest..\\.server\\.lan'`;
+cmp_ok(scalar @$json, 'eq', 0, 'Testing get after delete');
+
+done_testing();
diff --git a/t/edit.t b/t/edit.t
new file mode 100644
index 0000000..c58bccb
--- /dev/null
+++ b/t/edit.t
@@ -0,0 +1,26 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use v5.10;
+
+use JSON;
+use Test::More;
+
+chdir '..';
+my $json;
+my $ret;
+
+$json = decode_json `./mon --nocolor insert host set use = generic-host and name = footest.server.lan and host_name = footest.server.lan and address = 127.0.0.1`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing insert');
+
+$ret = `EDITOR=/bin/true ./mon --nocolor edit host where name like footest.server.lan`;
+cmp_ok($ret, 'eq', '', 'Testing edit');
+
+$ret = `EDITOR=/bin/true ./mon --nocolor edit host where name like footest.server.lan`;
+cmp_ok($ret, 'eq', '', 'Testing edit');
+
+$json = decode_json `./mon --nocolor delete host where name like footest.server.lan`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing delete');
+
+done_testing();
diff --git a/t/get.t b/t/get.t
new file mode 100644
index 0000000..85ae878
--- /dev/null
+++ b/t/get.t
@@ -0,0 +1,25 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use v5.10;
+
+use JSON;
+use Test::More;
+
+chdir '..';
+my $json;
+
+$json = decode_json `./mon --nocolor insert host set use = generic-host and name = footest.server.lan and host_name = footest.server.lan and address = 127.0.0.1`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing insert');
+
+$json = decode_json `./mon --nocolor get host where name like footest.server.lan`;
+cmp_ok($json->[0]{address}, 'eq', '127.0.0.1', 'Testing get');
+
+$json = decode_json `./mon --nocolor get host`;
+cmp_ok(scalar @$json, '>', '1', 'Testing get');
+
+$json = decode_json `./mon --nocolor delete host where name like footest.server.lan`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing delete');
+
+done_testing();
diff --git a/t/get_interactive.t b/t/get_interactive.t
new file mode 100644
index 0000000..85ae878
--- /dev/null
+++ b/t/get_interactive.t
@@ -0,0 +1,25 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use v5.10;
+
+use JSON;
+use Test::More;
+
+chdir '..';
+my $json;
+
+$json = decode_json `./mon --nocolor insert host set use = generic-host and name = footest.server.lan and host_name = footest.server.lan and address = 127.0.0.1`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing insert');
+
+$json = decode_json `./mon --nocolor get host where name like footest.server.lan`;
+cmp_ok($json->[0]{address}, 'eq', '127.0.0.1', 'Testing get');
+
+$json = decode_json `./mon --nocolor get host`;
+cmp_ok(scalar @$json, '>', '1', 'Testing get');
+
+$json = decode_json `./mon --nocolor delete host where name like footest.server.lan`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing delete');
+
+done_testing();
diff --git a/t/get_where.t b/t/get_where.t
new file mode 100644
index 0000000..58c0465
--- /dev/null
+++ b/t/get_where.t
@@ -0,0 +1,48 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use v5.10;
+
+use JSON;
+use Test::More;
+
+chdir '..';
+my $json;
+
+for my $num (1..10) {
+ $json = decode_json `./mon --nocolor insert host set use = generic-host and name = footest${num}.server.lan and host_name = footest${num}.server.lan and address = 127.0.0.${num}`;
+ cmp_ok($json->{message}, 'eq', '1 changes successfully commited', "Testing insert $num");
+}
+
+$json = decode_json `./mon --nocolor get host where name like footest1.server.lan`;
+cmp_ok($json->[0]{address}, 'eq', '127.0.0.1', 'Testing get like');
+
+$json = decode_json `./mon --nocolor get host where name matches footest1.server.lan`;
+cmp_ok($json->[0]{address}, 'eq', '127.0.0.1', 'Testing get matches');
+
+$json = decode_json `./mon --nocolor get host where name nmatches footest1.server.lan and name like footest`;
+cmp_ok(scalar @$json, '==', '9', 'Testing get nmatches');
+
+$json = decode_json `./mon --nocolor get host where name eq footest2.server.lan`;
+cmp_ok($json->[0]{address}, 'eq', '127.0.0.2', 'Testing get eq');
+
+$json = decode_json `./mon --nocolor get host where name ne footest1.server.lan and name like footest`;
+cmp_ok(scalar @$json, '==', '9', 'Testing get ne');
+
+$json = decode_json `./mon --nocolor get host where name lt 2 and name like footest`;
+cmp_ok(scalar @$json, '==', '1', 'Testing get lt');
+
+$json = decode_json `./mon --nocolor get host where name le 2 and name like footest`;
+cmp_ok(scalar @$json, '==', '2', 'Testing get le');
+
+$json = decode_json `./mon --nocolor get host where name gt 2 and name like footest`;
+cmp_ok(scalar @$json, '==', '8', 'Testing get gt');
+
+$json = decode_json `./mon --nocolor get host where name ge 2 and name like footest`;
+cmp_ok(scalar @$json, '==', '9', 'Testing get ge');
+
+$json = decode_json `./mon --nocolor delete host where name like footest and host_name like server.lan`;
+cmp_ok($json->{message}, 'eq', '10 changes successfully commited', 'Testing delete');
+
+done_testing();
diff --git a/t/getfmt.t b/t/getfmt.t
new file mode 100644
index 0000000..b696390
--- /dev/null
+++ b/t/getfmt.t
@@ -0,0 +1,23 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use v5.10;
+
+use JSON;
+use Test::More;
+
+chdir '..';
+my $json;
+my $res;
+
+$json = decode_json `./mon --nocolor insert host set use = generic-host and name = footest.server.lan and host_name = footest.server.lan and address = 127.0.0.1`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing insert');
+
+$res = `./mon --nocolor getfmt 'Foo:\$host_name' host where name like footest.server.lan`;
+like($res, qr/Foo:footest.server.lan/, 'Testing getfmt');
+
+$json = decode_json `./mon --nocolor delete host where name like footest.server.lan`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing delete');
+
+done_testing();
diff --git a/t/insert.t b/t/insert.t
new file mode 100644
index 0000000..451e0c4
--- /dev/null
+++ b/t/insert.t
@@ -0,0 +1,29 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use v5.10;
+
+use JSON;
+use Test::More;
+
+chdir '..';
+my $json;
+
+$json = decode_json `./mon --nocolor insert host set use = generic-host and name = footest.server.lan and host_name = footest.server.lan and address = 127.0.0.1 and _FOO = foo`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing insert');
+
+$json = decode_json `./mon --nocolor -m get host where name like footest.server.lan`;
+cmp_ok($json->[0]{_FOO}, 'eq', 'foo', 'Testing get after a put');
+
+$json = decode_json `./mon --nocolor insert host set use = generic-host and name = footest.server.lan and host_name = footest.server.lan and address = 127.0.0.2`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing insert');
+
+$json = decode_json `./mon --nocolor get host where name like footest.server.lan`;
+cmp_ok($json->[0]{address}, 'eq', '127.0.0.2', 'Testing get after a put');
+
+$json = decode_json `./mon --nocolor delete host where name like footest.server.lan`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing delete');
+
+done_testing();
+
diff --git a/t/options_debug_verbose_quiet.t b/t/options_debug_verbose_quiet.t
new file mode 100644
index 0000000..4ba69f8
--- /dev/null
+++ b/t/options_debug_verbose_quiet.t
@@ -0,0 +1,43 @@
+#!/usr/bin/perl
+
+# Testing --debug, -D, --verbose, -v and --quiet, -q
+
+use strict;
+use warnings;
+use v5.10;
+
+use JSON;
+use Test::More;
+
+chdir '..';
+my $json;
+my $ret;
+
+$ret = `./mon -q insert host set use = generic-host and name = footest.server.lan and host_name = footest.server.lan and address = 127.0.0.1`;
+cmp_ok($ret, 'eq', '', 'Testing quiet insert');
+
+$json = decode_json `./mon --nocolor get host where name like footest.server.lan`;
+cmp_ok($json->[0]{address}, 'eq', '127.0.0.1', 'Testing get');
+
+$json = decode_json `./mon --nocolor get host`;
+cmp_ok(scalar @$json, '>', '1', 'Testing get');
+
+$ret = `./mon -D --nocolor get host >/dev/null 2>/tmp/montest.tmp;cat /tmp/montest.tmp`;
+like($ret, qr/MON::Filter/, 'Testing debug get');
+
+$ret = `./mon --debug --nocolor get host >/dev/null 2>/tmp/montest.tmp;cat /tmp/montest.tmp`;
+like($ret, qr/MON::Filter/, 'Testing debug get');
+
+$ret = `./mon -v --nocolor get host >/dev/null 2>/tmp/montest.tmp;cat /tmp/montest.tmp`;
+like($ret, qr/Reading config/, 'Testing verbose get');
+
+$ret = `./mon --verbose --nocolor get host >/dev/null 2>/tmp/montest.tmp;cat /tmp/montest.tmp`;
+like($ret, qr/Reading config/, 'Testing verbose get');
+
+$ret = `./mon --quiet --nocolor delete host where name like footest.server.lan`;
+cmp_ok($ret, 'eq', '', 'Testing quiet delete');
+
+$json = decode_json `./mon --nocolor get host where name like footest.server.lan`;
+cmp_ok(scalar @$json, '<', '1', 'Testing get');
+
+done_testing();
diff --git a/t/options_version.t b/t/options_version.t
new file mode 100644
index 0000000..bfe45a1
--- /dev/null
+++ b/t/options_version.t
@@ -0,0 +1,28 @@
+#!/usr/bin/perl
+
+# Testing --version, -V
+
+use strict;
+use warnings;
+use v5.10;
+
+use JSON;
+use Test::More;
+
+chdir '..';
+my $json;
+my $ret;
+
+my $version = `cat .version`;
+chomp $version;
+
+$ret = `./mon --version`;
+chomp $ret;
+cmp_ok($ret, 'eq', $version, 'Testing version');
+
+$ret = `./mon -V`;
+chomp $ret;
+cmp_ok($ret, 'eq', $version, 'Testing version');
+
+
+done_testing();
diff --git a/t/post.t b/t/post.t
new file mode 100644
index 0000000..b27f2b5
--- /dev/null
+++ b/t/post.t
@@ -0,0 +1,41 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use v5.10;
+
+use File::Temp qw(tempfile);
+use JSON;
+use Test::More;
+
+my $json1 = <<JSON1;
+[
+ {
+ "address" : "127.0.0.1",
+ "host_name" : "footest.server.lan",
+ "hostgroups" : "",
+ "name" : "footest.server.lan",
+ "register" : "0"
+ }
+]
+JSON1
+
+chdir '..';
+
+my ($jsonfh1, $jsonfile1) = tempfile( undef, SUFFIX => '.json' );
+print $jsonfh1 $json1;
+
+my $json;
+
+$json = decode_json `./mon --nocolor post host < $jsonfile1`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing post');
+
+$json = decode_json `./mon --nocolor get host where name like footest.server.lan`;
+cmp_ok($json->[0]{address}, 'eq', '127.0.0.1', 'Testing get');
+
+$json = decode_json `./mon --nocolor delete host where name like footest.server.lan`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing delete');
+
+unlink $jsonfile1;
+
+done_testing();
diff --git a/t/post_as_array.t b/t/post_as_array.t
new file mode 100644
index 0000000..ede7b49
--- /dev/null
+++ b/t/post_as_array.t
@@ -0,0 +1,39 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use v5.10;
+
+use File::Temp qw(tempfile);
+use JSON;
+use Test::More;
+
+my $json1 = <<JSON1;
+[
+ "address", "127.0.0.1",
+ "host_name", "footest.server.lan",
+ "hostgroups", "",
+ "name", "footest.server.lan",
+ "register", "0"
+]
+JSON1
+
+chdir '..';
+
+my ($jsonfh1, $jsonfile1) = tempfile( undef, SUFFIX => '.json' );
+print $jsonfh1 $json1;
+
+my $json;
+
+$json = decode_json `./mon --nocolor post host < $jsonfile1`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing post');
+
+$json = decode_json `./mon --nocolor get host where name like footest.server.lan`;
+cmp_ok($json->[0]{address}, 'eq', '127.0.0.1', 'Testing get');
+
+$json = decode_json `./mon --nocolor delete host where name like footest.server.lan`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing delete');
+
+unlink $jsonfile1;
+
+done_testing();
diff --git a/t/post_interactive.t b/t/post_interactive.t
new file mode 100644
index 0000000..354d2b5
--- /dev/null
+++ b/t/post_interactive.t
@@ -0,0 +1,41 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use v5.10;
+
+use File::Temp qw(tempfile);
+use JSON;
+use Test::More;
+
+my $json1 = <<JSON1;
+[
+ {
+ "address" : "127.0.0.1",
+ "host_name" : "footest.server.lan",
+ "hostgroups" : "",
+ "name" : "footest.server.lan",
+ "register" : "0"
+ }
+]
+JSON1
+
+chdir '..';
+
+my ($jsonfh1, $jsonfile1) = tempfile( undef, SUFFIX => '.json' );
+print $jsonfh1 $json1;
+
+my $json;
+
+$json = decode_json `./mon --nocolor post host \< $jsonfile1`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing post');
+
+$json = decode_json `./mon --nocolor get host where name like footest.server.lan`;
+cmp_ok($json->[0]{address}, 'eq', '127.0.0.1', 'Testing get');
+
+$json = decode_json `./mon --nocolor delete host where name like footest.server.lan`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing delete');
+
+unlink $jsonfile1;
+
+done_testing();
diff --git a/t/put.t b/t/put.t
new file mode 100644
index 0000000..9f07180
--- /dev/null
+++ b/t/put.t
@@ -0,0 +1,60 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use v5.10;
+
+use File::Temp qw(tempfile);
+use JSON;
+use Test::More;
+
+my $json1 = <<JSON1;
+[
+ {
+ "address" : "127.0.0.1",
+ "host_name" : "footest.server.lan",
+ "hostgroups" : "",
+ "name" : "footest.server.lan",
+ "register" : "0"
+ }
+]
+JSON1
+
+my $json2 = <<JSON2;
+[
+ {
+ "address" : "127.0.0.2",
+ "name" : "footest.server.lan"
+ }
+]
+JSON2
+
+chdir '..';
+
+my ($jsonfh1, $jsonfile1) = tempfile( undef, SUFFIX => '.json' );
+print $jsonfh1 $json1;
+
+my ($jsonfh2, $jsonfile2) = tempfile( undef, SUFFIX => '.json' );
+print $jsonfh2 $json2;
+
+my $json;
+
+$json = decode_json `./mon --nocolor post host < $jsonfile1`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing post');
+
+$json = decode_json `./mon --nocolor get host where name like footest.server.lan`;
+cmp_ok($json->[0]{address}, 'eq', '127.0.0.1', 'Testing get');
+
+$json = decode_json `./mon --nocolor put host < $jsonfile2`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing put');
+
+$json = decode_json `./mon --nocolor get host where name like footest.server.lan`;
+cmp_ok($json->[0]{address}, 'eq', '127.0.0.2', 'Testing get after a put');
+
+$json = decode_json `./mon --nocolor delete host where name like footest.server.lan`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing delete');
+
+unlink $jsonfile1;
+unlink $jsonfile2;
+
+done_testing();
diff --git a/t/update.t b/t/update.t
new file mode 100644
index 0000000..1593371
--- /dev/null
+++ b/t/update.t
@@ -0,0 +1,49 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use v5.10;
+
+use JSON;
+use Test::More;
+
+chdir '..';
+my $json;
+
+$json = decode_json `./mon --nocolor insert host set use = generic-host and name = footest.server.lan and host_name = footest.server.lan and address = 127.0.0.1 and _FOO = foo and _BAR = bar`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing insert');
+
+$json = decode_json `./mon --nocolor update host delete _FOO where name like footest.server.lan`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing update ... delete');
+
+$json = decode_json `./mon --nocolor -m get host where name like footest.server.lan`;
+cmp_ok(exists $json->[0]{_FOO}, '==', 0, 'Testing get after a update');
+cmp_ok($json->[0]{_BAR}, 'eq', 'bar', 'Testing get after a update');
+
+$json = decode_json `./mon --nocolor update host set _FOO = fuu where name like footest.server.lan`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing update ... set');
+
+$json = decode_json `./mon --nocolor -m get host where name like footest.server.lan`;
+cmp_ok($json->[0]{_FOO}, 'eq', 'fuu', 'Testing get after a update');
+
+$json = decode_json `./mon --nocolor update host set _ONE = one and _TWO = two where name like footest.server.lan`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing update ... set with multiple objects');
+
+$json = decode_json `./mon --nocolor -m get host where name like footest.server.lan`;
+cmp_ok($json->[0]{_FOO}, 'eq', 'fuu', 'Testing get after a update');
+cmp_ok($json->[0]{_BAR}, 'eq', 'bar', 'Testing get after a update');
+cmp_ok($json->[0]{_ONE}, 'eq', 'one', 'Testing get after a update');
+cmp_ok($json->[0]{_TWO}, 'eq', 'two', 'Testing get after a update');
+
+$json = decode_json `./mon --nocolor update host delete _FOO and _BAR where name like footest.server.lan`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing update ... delete with multiple arguments');
+
+$json = decode_json `./mon --nocolor -m get host where name like footest.server.lan`;
+cmp_ok(exists $json->[0]{_FOO}, '==', 0, 'Testing get after a update');
+cmp_ok(exists $json->[0]{_BAR}, '==', 0, 'Testing get after a update');
+
+$json = decode_json `./mon --nocolor delete host where name like footest.server.lan`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing put');
+
+done_testing();
+
diff --git a/t/update_format.t b/t/update_format.t
new file mode 100644
index 0000000..248dd17
--- /dev/null
+++ b/t/update_format.t
@@ -0,0 +1,37 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use v5.10;
+
+use JSON;
+use Test::More;
+
+chdir '..';
+my $json;
+
+$json = decode_json `./mon --nocolor insert host set use = generic-host and name = footest01.server.lan and host_name = footest01.server.lan and address = 127.0.0.1 and _FOO = foo and _BAR = bar`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing insert');
+
+$json = decode_json `./mon --nocolor insert host set use = generic-host and name = footest02.server.lan and host_name = footest02.server.lan and address = 127.0.0.1 and _FOO = foo and _BAR = bar`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing insert');
+
+$json = decode_json `./mon --nocolor -m update host set _BEER = '\@_FOO' and _PIZZA = '\$_BAR' and _COLA = '\${_FOO}foo\@{_BAR}bar' where name like footest and host_name like server.lan`;
+cmp_ok($json->{message}, 'eq', '2 changes successfully commited', 'Testing expression in update ... set');
+
+$json = decode_json `./mon --nocolor -m get host where name like footest and host_name like server.lan`;
+cmp_ok($json->[0]{_BEER}, 'eq', 'foo', 'Testing get after a update');
+cmp_ok($json->[1]{_BEER}, 'eq', 'foo', 'Testing get after a update');
+cmp_ok($json->[0]{_PIZZA}, 'eq', 'bar', 'Testing get after a update');
+cmp_ok($json->[1]{_PIZZA}, 'eq', 'bar', 'Testing get after a update');
+cmp_ok($json->[0]{_COLA}, 'eq', 'foofoobarbar', 'Testing get after a update');
+cmp_ok($json->[1]{_COLA}, 'eq', 'foofoobarbar', 'Testing get after a update');
+
+$json = decode_json `./mon --nocolor delete host where name like footest01.server.lan`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing put');
+
+$json = decode_json `./mon --nocolor delete host where name like footest02.server.lan`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing put');
+
+done_testing();
+
diff --git a/t/view.t b/t/view.t
new file mode 100644
index 0000000..5351b2c
--- /dev/null
+++ b/t/view.t
@@ -0,0 +1,22 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use v5.10;
+
+use JSON;
+use Test::More;
+
+chdir '..';
+my ($json, $out);
+
+$json = decode_json `./mon --nocolor insert host set use = generic-host and name = footest.server.lan and host_name = footest.server.lan and address = 127.0.0.1`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing insert');
+
+$out = `PAGER=/bin/true ./mon --nocolor view host where name like footest.server.lan 2>&1`;
+cmp_ok($out, 'eq', '', 'Testing view');
+
+$json = decode_json `./mon --nocolor delete host where name like footest.server.lan`;
+cmp_ok($json->{message}, 'eq', '1 changes successfully commited', 'Testing delete');
+
+done_testing();