Last active
February 21, 2020 16:35
-
-
Save kjetilho/c1503945a43841069d26177f02edcea6 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /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