Skip to content

Instantly share code, notes, and snippets.

@kjetilho
Last active February 21, 2020 16:35
Show Gist options
  • Save kjetilho/c1503945a43841069d26177f02edcea6 to your computer and use it in GitHub Desktop.
Save kjetilho/c1503945a43841069d26177f02edcea6 to your computer and use it in GitHub Desktop.
#! /usr/bin/perl
#
# puppetdb 1.0 - a wrapper to simplify lookups in PuppetDB.
# Written 2018 by [email protected]
use Getopt::Long;
use LWP::UserAgent;
use URI::Escape;
use JSON;
use strict;
use warnings;
my %backends = (
2 => 'http://puppetdb.i.bitbit.net/v3',
3 => 'http://puppetdb.i.bitbit.net/v3',
5 => 'http://puppetdb5.i.bitbit.net/pdb/query/v4',
merge => 'https://puppetdb-proxy-services.apps.bitbit.net'
);
my $toplevel = "certname environment exported file line tag title type";
sub usage {
if (@_) {
print STDERR "ERROR: ", @_, "\n\n";
}
print STDERR <<"END";
Usage: $0 [MODE] [-b BACKEND] [-h HOSTNAME] [-t RESOURCETYPE [-n RESOURCENAME]] [QUERY ...]
Options:
--[no-]pretty Make sure JSON has nice indentation. In facts mode,
transform array into more useful hash.
* BACKEND is one of "2" (same as "3"), "5" or "merge" (default).
* RESOURCETYPE is a type like File or define like bareos::client_definition
* RESOURCENAME is the title of that type, like "/etc/motd"
* QUERY is a dumbed down query syntax, a sequence of "variable OP value"
which are ANDed together. OP can be one of
= (equality)
~ (regexp match)
For the "line" variable you may also use <, >, <=, >= (but WHY!).
Known top level variables are
$toplevel
Other variable names will be assumed to be parameters to the type or define.
At least one of HOSTNAME and RESOURCETYPE should be specified, since
getting the full data set is resource intensive.
MODE can also be
--facts look up facts. In this mode, -t specifies fact name,
and -n specifies fact value.
--report[=N] return metadata about last N reports (runs) to PuppetDB.
Use negative value to get oldest runs.
END
# https://docs.puppet.com/puppetdb/1.5/api/query/v2/operators.html
exit(64);
}
sub Title_Case {
my $string = shift;
join("", map { ucfirst } split(/([^a-z0-9_]+)/i, lc $string));
}
my ($restype, $resname, $hostname);
my $backend = 'merge';
my $pretty_print = 1;
my $debug = 0;
my $force = 0;
my $report;
my $facts;
GetOptions('hostname|h=s', \$hostname,
'resource-type|t=s', \$restype,
'resource-name|n=s', \$resname,
'report:1', \$report,
'facts', \$facts,
'backend|b=s', \$backend,
'force!', \$force,
'pretty!', \$pretty_print,
'debug|d+', \$debug,
) or usage();
usage("--resource-name requires --resource-type")
if $resname && !$restype;
usage("report lookup requires hostname")
if $report && !$hostname;
usage("data lookup must be restricted")
unless $facts || $report || $restype || $hostname || $force || $backend eq '5'; # 5 is small enough for now
usage("Unknown backend '$backend'")
unless $backends{$backend};
my $url = '';
if ($facts) {
# environment is only available in Puppet 5
$toplevel = "certname value name environment";
$url .= '/facts';
unshift(@ARGV, "certname=$hostname") if $hostname;
$url .= "/$restype" if $restype;
$url .= "/$resname" if $resname;
} elsif ($report) {
$url .= '/reports';
unshift(@ARGV, "certname=$hostname");
} else {
if ($hostname) {
$url .= "/nodes/" . lc $hostname;
}
if ($restype) {
$url .= '/resources/' . Title_Case($restype);
if ($resname) {
if ($restype eq 'Class') {
$resname = Title_Case($resname);
}
if ($resname =~ m:/:) {
# workaround for defective direct URL (e.g., File//etc/passwd)
unshift(@ARGV, "title=$resname");
} else {
$url .= '/' . $resname;
}
}
} else {
$url .= '/resources';
}
}
my @cond;
for (@ARGV) {
if (/(\S+?)\s*([<>]=?|=|!=|<>|~)\s*(.*)/) {
my ($param, $op, $value) = ($1, $2, $3);
my $negate;
if ($op eq '!=' || $op eq '<>') {
$negate = 1;
$op = '=';
}
if ($value =~ /^(true|false|\d+)$/) {
# use it plain
} else {
$value = "\"$value\"";
}
my $expr;
if ($toplevel =~ /\b$param\b/) {
$expr = sprintf('["%s", "%s", %s]', $op, $param, $value);
} else {
$expr = sprintf('["%s", ["parameter", "%s"], %s]', $op, $param, $value);
}
$expr = "[\"not\", $expr]"
if $negate;
push(@cond, $expr);
} else {
print STDERR "invalid query: $_\n";
usage();
}
}
use Data::Dumper;
print STDERR Dumper(\@cond) if $debug > 1;
my $full_url = $backends{$backend} . $url;
if (@cond) {
my $query;
if (@cond == 1) {
$query = $cond[0];
} else {
$query = sprintf('["and", %s]', join(', ', @cond));
}
$full_url .= "?query=" . uri_escape($query);
}
my $ua = LWP::UserAgent->new;
$ua->default_header("Accept" => "application/json");
print STDERR "GET $full_url\n" if $debug;
my $resp = $ua->get($full_url);
if ($resp->is_success) {
my $result = $resp->decoded_content;
if ($pretty_print || $report) {
$result = JSON::decode_json($result);
if ($report) {
# Puppet 3 (report-format 4) has receive-time, Puppet 5
# (report-format 10) has receive_time. We can have a mix
# in the result.
my @selection = sort { ($b->{"receive-time"} || $b->{"receive_time"}) cmp
($a->{"receive-time"} || $a->{"receive_time"}) } @{$result};
if ($report < 0) {
@selection = reverse @selection;
$report = -$report;
}
if ($report < @selection) {
@selection = @selection[0 .. $report-1];
}
$result = \@selection;
} elsif ($facts && ($hostname || $restype)) {
my %facts = ();
for my $f (@{$result}) {
my $key;
if ($hostname) {
$key = $f->{name}; # fact name
} elsif ($restype) { # filter on fact name(!)
$key = $f->{certname};
}
if (exists $facts{$key}) {
unless (ref $facts{$key}) {
$facts{$key} = [ $facts{$key} ];
}
push(@{$facts{$key}}, $f->{value});
} else {
$facts{$key} = $f->{value}
}
}
$result = \%facts;
}
print JSON->new->pretty->encode($result);
} else {
print $result, "\n";
}
} else {
print $resp->content if $debug;
die $resp->status_line;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment