File Coverage

blib/lib/App/ElasticSearch/Utilities.pm
Criterion Covered Total %
statement 145 491 29.5
branch 65 362 17.9
condition 3 121 2.4
subroutine 29 60 48.3
pod 30 30 100.0
total 272 1064 25.5


line stmt bran cond sub pod time code
1             # ABSTRACT: Utilities for Monitoring ElasticSearch
2             package App::ElasticSearch::Utilities;
3              
4 4     4   1305 use v5.10;
  4         31  
5 4     4   20 use strict;
  4         8  
  4         88  
6 4     4   17 use warnings;
  4         10  
  4         198  
7              
8             our $VERSION = '8.5'; # VERSION
9              
10 4     4   1649 use App::ElasticSearch::Utilities::HTTPRequest;
  4         14  
  4         170  
11 4     4   2234 use CLI::Helpers qw(:all);
  4         373335  
  4         32  
12 4     4   901 use Getopt::Long qw(GetOptionsFromArray :config pass_through no_auto_abbrev);
  4         11  
  4         35  
13 4     4   2811 use Hash::Flatten qw(flatten);
  4         10003  
  4         256  
14 4     4   1829 use Hash::Merge::Simple qw(clone_merge);
  4         2323  
  4         236  
15 4     4   2064 use IPC::Run3;
  4         13575  
  4         220  
16 4     4   40 use JSON::MaybeXS;
  4         17  
  4         214  
17 4     4   2981 use LWP::UserAgent;
  4         116090  
  4         146  
18 4     4   2000 use Net::Netrc;
  4         17389  
  4         157  
19 4     4   35 use Ref::Util qw(is_ref is_arrayref is_hashref);
  4         10  
  4         265  
20 4     4   26 use Time::Local;
  4         30  
  4         242  
21 4     4   29 use URI;
  4         7  
  4         117  
22 4     4   1884 use URI::QueryParam;
  4         3176  
  4         126  
23 4     4   1668 use YAML::XS ();
  4         11617  
  4         442  
24              
25             # Control loading ARGV
26             my $ARGV_AT_INIT = 1;
27             my $COPY_ARGV = 0;
28             our $_init_complete = 0;
29              
30 4         63 use Sub::Exporter -setup => {
31             collectors => [
32             copy_argv => \'_copy_argv',
33             preprocess_argv => \'_preprocess_argv',
34             delay_argv => \'_delay_argv',
35             ],
36             exports => [ qw(
37             es_utils_initialize
38             es_globals
39             es_basic_auth
40             es_pattern
41             es_connect
42             es_master
43             es_request
44             es_nodes
45             es_indices
46             es_indices_meta
47             es_index_valid
48             es_index_bases
49             es_index_strip_date
50             es_index_days_old
51             es_index_shards
52             es_index_segments
53             es_index_stats
54             es_index_fields
55             es_settings
56             es_node_stats
57             es_segment_stats
58             es_close_index
59             es_open_index
60             es_delete_index
61             es_optimize_index
62             es_apply_index_settings
63             es_local_index_meta
64             es_flatten_hash
65             )],
66             groups => {
67             config => [qw(es_utils_initialize es_globals)],
68             default => [qw(es_utils_initialize es_connect es_indices es_request)],
69             indices => [qw(:default es_indices_meta)],
70             index => [qw(:default es_index_valid es_index_fields es_index_days_old es_index_bases)],
71             },
72 4     4   3035 };
  4         45182  
73 4     4   6270 use App::ElasticSearch::Utilities::Connection;
  4         3664  
  4         195  
74 4     4   1757 use App::ElasticSearch::Utilities::VersionHacks qw(_fix_version_request);
  4         23  
  4         36  
75              
76             # Collectors
77 0     0   0 sub _copy_argv { $COPY_ARGV = 1 }
78 0     0   0 sub _preprocess_argv { $ARGV_AT_INIT = 1 }
79 0     0   0 sub _delay_argv { $ARGV_AT_INIT = 0 }
80              
81              
82             # Global Variables
83             our %_GLOBALS = ();
84             my %DEF = ();
85             my %PATTERN_REGEX = (
86             '*' => qr/.*/,
87             ANY => qr/.*/,
88             );
89             my $PATTERN;
90              
91             {
92             ## no critic (ProhibitNoWarnings)
93 4     4   1384 no warnings;
  4         10  
  4         26327  
94             INIT {
95 4 50   4   57 return if $_init_complete++;
96 4 50       26 es_utils_initialize() if $ARGV_AT_INIT;
97             }
98             ## use critic
99             }
100              
101              
102             {
103             # Argument Parsing Block
104             my @argv_original = ();
105             my $parsed_argv = 0;
106             sub _parse_options {
107 4     4   13 my ($opt_ref) = @_;
108 4         49 my @opt_spec = qw(
109             local
110             host=s
111             port=i
112             timeout=i
113             keep-proxy
114             index=s
115             pattern=s
116             base|index-basename=s
117             days=i
118             noop!
119             datesep|date-separator=s
120             proto=s
121             http-username=s
122             password-exec=s
123             master-only|M
124             insecure
125             capath=s
126             cacert=s
127             cert=s
128             key=s
129             );
130              
131 4         10 my $argv;
132             my %opt;
133 4 50 33     23 if( defined $opt_ref && is_arrayref($opt_ref) ) {
134             # If passed an argv array, use that
135 0 0       0 $argv = $COPY_ARGV ? [ @{ $opt_ref } ] : $opt_ref;
  0         0  
136             }
137             else {
138             # Ensure multiple calls to cli_helpers_initialize() yield the same results
139 4 50       15 if ( $parsed_argv ) {
140             ## no critic
141 0         0 @ARGV = @argv_original;
142             ## use critic
143             }
144             else {
145 4         9 @argv_original = @ARGV;
146 4         10 $parsed_argv++;
147             }
148             # Operate on @ARGV
149 4 50       16 $argv = $COPY_ARGV ? [ @ARGV ] : \@ARGV;
150             }
151 4         31 GetOptionsFromArray($argv, \%opt, @opt_spec );
152 4         5336 return \%opt;
153             }
154             }
155              
156              
157             sub es_utils_initialize {
158 4     4 1 12 my ($argv) = @_;
159              
160             # Parse Options
161 4         16 my $opts = _parse_options($argv);
162              
163             # Config file locations
164 4         20 my @configs = (
165             '/etc/es-utils.yaml',
166             '/etc/es-utils.yml',
167             );
168 4 50       22 if( $ENV{HOME} ) {
169 4         14 push @configs, map { "$ENV{HOME}/.es-utils.$_" } qw( yaml yml );
  8         36  
170 4   33     32 my $xdg_config_home = $ENV{XDG_CONFIG_HOME} || "$ENV{HOME}/.config";
171 4         14 push @configs, map { "${xdg_config_home}/es-utils/config.$_" } qw( yaml yml );
  8         26  
172             }
173              
174 4         13 my @ConfigData=();
175 4         13 foreach my $config_file (@configs) {
176 24 50       3726 next unless -f $config_file;
177 0         0 debug("Loading options from $config_file");
178             eval {
179 0         0 my $ref = YAML::XS::LoadFile($config_file);
180 0         0 push @ConfigData, $ref;
181 0         0 debug_var($ref);
182 0         0 1;
183 0 0       0 } or do {
184 0         0 debug({color=>"red"}, "[$config_file] $@");
185             };
186             }
187 4 50       27 %_GLOBALS = @ConfigData ? %{ clone_merge(@ConfigData) } : ();
  0         0  
188              
189             # Set defaults
190             %DEF = (
191             # Connection Options
192             HOST => exists $opts->{host} ? $opts->{host}
193             : exists $opts->{local} ? 'localhost'
194             : exists $_GLOBALS{host} ? $_GLOBALS{host}
195             : 'localhost',
196             PORT => exists $opts->{port} ? $opts->{port}
197             : exists $_GLOBALS{port} ? $_GLOBALS{port}
198             : 9200,
199             PROTO => exists $opts->{proto} ? $opts->{proto}
200             : exists $_GLOBALS{proto} ? $_GLOBALS{proto}
201             : 'http',
202             TIMEOUT => exists $opts->{timeout} ? $opts->{timeout}
203             : exists $_GLOBALS{timeout} ? $_GLOBALS{timeout}
204             : 10,
205             NOOP => exists $opts->{noop} ? $opts->{noop}
206             : exists $_GLOBALS{noop} ? $_GLOBALS{noop}
207             : undef,
208             NOPROXY => exists $opts->{'keep-proxy'} ? 0
209             : exists $_GLOBALS{'keep-proxy'} ? $_GLOBALS{'keep-proxy'}
210             : 1,
211             MASTERONLY => exists $opts->{'master-only'} ? $opts->{'master-only'} : 0,
212             # Index selection opts->ions
213             INDEX => exists $opts->{index} ? $opts->{index} : undef,
214             BASE => exists $opts->{base} ? lc $opts->{base}
215             : exists $_GLOBALS{base} ? $_GLOBALS{base}
216             : undef,
217             PATTERN => exists $opts->{pattern} ? $opts->{pattern} : '*',
218             DAYS => exists $opts->{days} ? $opts->{days}
219             : exists $_GLOBALS{days} ? $_GLOBALS{days} : 1,
220             DATESEP => exists $opts->{datesep} ? $opts->{datesep}
221             : exists $_GLOBALS{datesep} ? $_GLOBALS{datesep}
222             : exists $_GLOBALS{"date-separator"} ? $_GLOBALS{"date-separator"}
223             : '.',
224             # HTTP Basic Authentication
225             USERNAME => exists $opts->{'http-username'} ? $opts->{'http-username'}
226             : exists $_GLOBALS{'http-username'} ? $_GLOBALS{'http-username'}
227             : $ENV{USER},
228             PASSEXEC => exists $opts->{'password-exec'} ? $opts->{'password-exec'}
229             : exists $_GLOBALS{'password-exec'} ? $_GLOBALS{'password-exec'}
230             : undef,
231             # TLS Options
232             INSECURE => exists $opts->{insecure} ? 1
233             : exists $_GLOBALS{insecure} ? $_GLOBALS{insecure}
234             : 0,
235             CACERT => exists $opts->{cacert} ? 1
236             : exists $_GLOBALS{cacert} ? $_GLOBALS{cacert}
237             : undef,
238             CAPATH => exists $opts->{capath} ? 1
239             : exists $_GLOBALS{capath} ? $_GLOBALS{capath}
240             : undef,
241             CERT => exists $opts->{cert} ? 1
242             : exists $_GLOBALS{cert} ? $_GLOBALS{cert}
243             : undef,
244             KEY => exists $opts->{key} ? 1
245             : exists $_GLOBALS{key} ? $_GLOBALS{key}
246             : undef,
247 4 50       278 );
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
248 4 50       24 CLI::Helpers::override(verbose => 1) if $DEF{NOOP};
249              
250 4 50       17 if( $DEF{NOPROXY} ) {
251 4         25 debug("Removing any active HTTP Proxies from ENV.");
252 4         67905 delete $ENV{$_} for qw(http_proxy HTTP_PROXY);
253             }
254              
255             # Setup Variables based on the config
256             %PATTERN_REGEX = (
257 4         125 '*' => qr/.*/,
258             DATE => qr/\d{4}(?:\Q$DEF{DATESEP}\E)?\d{2}(?:\Q$DEF{DATESEP}\E)?\d{2}/,
259             ANY => qr/.*/,
260             );
261 4         19 my @ordered = qw(* DATE ANY);
262              
263 4 50       41 if( index($DEF{DATESEP},'-') >= 0 ) {
264 0         0 output({stderr=>1,color=>'yellow'}, "=== Using a '-' as your date separator may cause problems with other utilities. ===");
265             }
266              
267             # Build the Index Pattern
268 4         12 $PATTERN = $DEF{PATTERN};
269 4         14 foreach my $literal ( @ordered ) {
270 12         158 $PATTERN =~ s/\Q$literal\E/$PATTERN_REGEX{$literal}/g;
271             }
272              
273             }
274              
275             # Regexes for Pattern Expansion
276             our $CURRENT_VERSION;
277             my $CLUSTER_MASTER;
278              
279              
280             sub es_globals {
281 1     1 1 4 my ($key) = @_;
282              
283 1 50       6 es_utils_initialize() unless keys %DEF;
284              
285 1 50       6 return unless exists $_GLOBALS{$key};
286 0         0 return $_GLOBALS{$key};
287             }
288              
289              
290             my %_auth_cache = ();
291              
292             sub es_basic_auth {
293 0     0 1 0 my ($host) = @_;
294              
295 0 0       0 es_utils_initialize() unless keys %DEF;
296              
297 0   0     0 $host ||= $DEF{HOST};
298              
299             # Return the results if we've done this already
300 0         0 return @{ $_auth_cache{$host} }{qw(username password)}
301 0 0       0 if exists $_auth_cache{$host};
302              
303             # Set the cached element
304 0         0 my %auth = ();
305              
306             # Lookup the details netrc
307 0         0 my $netrc = Net::Netrc->lookup($host);
308 0 0       0 if( $DEF{HOST} eq $host ) {
309 0         0 %auth = map { lc($_) => $DEF{$_} } qw(USERNAME);
  0         0  
310             }
311 0         0 my %meta = ();
312 0         0 foreach my $k (qw( http-username password-exec )) {
313 0         0 foreach my $name ( $DEF{INDEX}, $DEF{BASE} ) {
314 0 0       0 next unless $name;
315 0 0       0 if( my $v = es_local_index_meta($k, $name) ) {
316 0         0 $meta{$k} = $v;
317 0         0 last;
318             }
319             }
320             }
321              
322             # Get the Username
323             $auth{username} ||= $meta{'http-username'} ? $meta{'http-username'}
324             : defined $DEF{USERNAME} ? $DEF{USERNAME}
325             : defined $netrc ? $netrc->login
326 0 0 0     0 : $ENV{USER};
    0          
    0          
327              
328             # Prompt for the password
329             $auth{password} ||= defined $netrc ? $netrc->password
330             : (es_pass_exec($host,$auth{username},$meta{'password-exec'})
331 0 0 0     0 || prompt(sprintf "Password for '%s' at '%s': ", $auth{username}, $host)
      0        
332             );
333              
334             # Store
335 0         0 $_auth_cache{$host} = \%auth;
336 0         0 return @auth{qw(username password)};
337             }
338              
339              
340             sub es_pass_exec {
341 0     0 1 0 my ($host,$username,$exec) = @_;
342              
343 0 0       0 es_utils_initialize() unless keys %DEF;
344              
345             # Simplest case we can't run
346 0   0     0 $exec ||= $DEF{PASSEXEC};
347 0 0 0     0 return unless length $exec && -x $exec;
348              
349 0         0 my(@out,@err);
350             # Run the password command captue out, error and RC
351 0         0 run3 [ $exec, $host, $username ], \undef, \@out, \@err;
352 0         0 my $rc = $?;
353              
354             # Record the error
355 0 0 0     0 if( @err or $rc != 0 ) {
356 0         0 output({color=>'red',stderr=>1},
357             sprintf("es_pass_exec() called '%s' and met with an error code '%d'", $exec, $rc),
358             @err
359             );
360 0         0 return;
361             }
362              
363             # Format and return the result
364 0         0 my $passwd = $out[-1];
365 0         0 chomp($passwd);
366 0         0 return $passwd;
367             }
368              
369              
370              
371             sub es_pattern {
372 0 0   0 1 0 es_utils_initialize() unless keys %DEF;
373             return {
374             re => $PATTERN,
375             string => $DEF{PATTERN},
376 0         0 };
377             }
378              
379             sub _get_ssl_opts {
380 0 0   0   0 es_utils_initialize() unless keys %DEF;
381 0         0 my %opts = ();
382 0 0       0 $opts{verify_hostname} = 0 if $DEF{INSECURE};
383 0 0       0 $opts{SSL_ca_file} = $DEF{CACERT} if $DEF{CACERT};
384 0 0       0 $opts{SSL_ca_path} = $DEF{CAPATH} if $DEF{CAPATH};
385 0 0       0 $opts{SSL_cert_file} = $DEF{CERT} if $DEF{CERT};
386 0 0       0 $opts{SSL_key_file} = $DEF{KEY} if $DEF{KEY};
387 0         0 return \%opts;
388             }
389              
390             sub _get_es_version {
391 0 0   0   0 return $CURRENT_VERSION if defined $CURRENT_VERSION;
392 0         0 my $conn = es_connect();
393             # Build the request
394 0         0 my $req = App::ElasticSearch::Utilities::HTTPRequest->new(
395             GET => sprintf "%s://%s:%d",
396             $conn->proto, $conn->host, $conn->port
397             );
398             # Check if we're doing auth
399 0 0       0 my @auth = $DEF{PASSEXEC} ? es_basic_auth($conn->host) : ();
400             # Add authentication if we get a password
401 0 0       0 $req->authorization_basic( @auth ) if @auth;
402              
403             # Retry with TLS and/or Auth
404 0         0 my %try = map { $_ => 1 } qw( tls auth );
  0         0  
405 0         0 my $resp;
406 0         0 while( not defined $CURRENT_VERSION ) {
407 0         0 $resp = $conn->ua->request($req);
408 0 0 0     0 if( $resp->is_success ) {
    0          
    0          
409 0         0 my $ver;
410 0         0 eval {
411 0         0 $ver = $resp->content->{version};
412             };
413 0 0       0 if( $ver ) {
414 0 0 0     0 if( $ver->{distribution} and $ver->{distribution} eq 'opensearch' ) {
415 0         0 $CURRENT_VERSION = '7.10';
416             }
417             else {
418 0         0 $CURRENT_VERSION = join('.', (split /\./,$ver->{number})[0,1]);
419             }
420             }
421             }
422             elsif( $resp->code == 500 && $resp->message eq "Server closed connection without sending any data back" ) {
423             # Try TLS
424 0 0       0 last unless $try{tls};
425 0         0 delete $try{tls};
426 0         0 $conn->proto('https');
427 0         0 warn "Attempting promotion to HTTPS, try setting 'proto: https' in ~/.es-utils.yaml";
428             }
429             elsif( $resp->code == 401 ) {
430             # Retry with credentials
431 0 0       0 last unless $try{auth};
432 0         0 delete $try{auth};
433             warn "Authorization required, try setting 'password-exec: /home/user/bin/get-password.sh` in ~/.es-utils.yaml'"
434 0 0       0 unless $DEF{PASSEXEC};
435 0         0 $req->authorization_basic( es_basic_auth($conn->host) );
436             }
437             else {
438 0         0 warn "Failed getting version";
439 0         0 last;
440             }
441             }
442 0 0 0     0 if( !defined $CURRENT_VERSION || $CURRENT_VERSION <= 2 ) {
443 0         0 output({color=>'red',stderr=>1}, sprintf "[%d] Unable to determine Elasticsearch version, something has gone terribly wrong: aborting.", $resp->code);
444 0 0       0 output({color=>'red',stderr=>1}, ref $resp->content ? YAML::XS::Dump($resp->content) : $resp->content) if $resp->content;
    0          
445 0         0 exit 1;
446             }
447 0         0 debug({color=>'magenta'}, "FOUND VERISON '$CURRENT_VERSION'");
448 0         0 return $CURRENT_VERSION;
449             }
450              
451              
452             my $ES = undef;
453              
454             sub es_connect {
455 0     0 1 0 my ($override_servers) = @_;
456              
457 0 0       0 es_utils_initialize() unless keys %DEF;
458              
459             my %conn = (
460             host => $DEF{HOST},
461             port => $DEF{PORT},
462             proto => $DEF{PROTO},
463             timeout => $DEF{TIMEOUT},
464 0         0 ssl_opts => _get_ssl_opts,
465             );
466             # Only authenticate over TLS
467 0 0       0 if( $DEF{PROTO} eq 'https' ) {
468 0         0 $conn{username} = $DEF{USERNAME};
469 0 0       0 $conn{password} = es_pass_exec(@DEF{qw(HOST USERNAME)}) if $DEF{PASSEXEC};
470             }
471              
472             # If we're overriding, return a unique handle
473 0 0       0 if(defined $override_servers) {
474 0 0       0 my @overrides = is_arrayref($override_servers) ? @$override_servers : $override_servers;
475 0         0 my @servers;
476 0         0 foreach my $entry ( @overrides ) {
477 0         0 my ($s,$p) = split /\:/, $entry;
478 0   0     0 $p ||= $conn{port};
479 0         0 push @servers, { %conn, host => $s, port => $p };
480             }
481              
482 0 0       0 if( @servers > 0 ) {
483 0 0       0 my $pick = @servers > 1 ? $servers[int(rand(@servers))] : $servers[0];
484 0         0 return App::ElasticSearch::Utilities::Connection->new(%{$pick});
  0         0  
485             }
486             }
487             else {
488             # Check for index metadata
489 0         0 foreach my $k ( keys %conn ) {
490 0         0 foreach my $name ( $DEF{INDEX}, $DEF{BASE} ) {
491 0 0       0 next unless $name;
492 0 0       0 if( my $v = es_local_index_meta($k => $name) ) {
493 0         0 $conn{$k} = $v;
494 0         0 last;
495             }
496             }
497             }
498             }
499              
500             # Otherwise, cache our handle
501 0   0     0 $ES ||= App::ElasticSearch::Utilities::Connection->new(%conn);
502              
503 0         0 return $ES;
504             }
505              
506              
507             sub es_master {
508 0     0 1 0 my ($instance) = @_;
509 0 0 0     0 if(!defined $instance && defined $CLUSTER_MASTER) {
510 0         0 return $CLUSTER_MASTER;
511             }
512 0         0 my $is_master = 0;
513 0         0 my @request = ('/_cluster/state/master_node');
514 0 0       0 unshift @request, $instance if defined $instance;
515              
516 0         0 my $cluster = es_request(@request);
517 0 0 0     0 if( defined $cluster && $cluster->{master_node} ) {
518 0         0 my $local = es_request('/_nodes/_local');
519 0 0 0     0 if ($local->{nodes} && $local->{nodes}{$cluster->{master_node}}) {
520 0         0 $is_master = 1;
521             }
522             }
523 0 0       0 $CLUSTER_MASTER = $is_master unless defined $instance;
524 0         0 return $is_master;
525             }
526              
527              
528             sub es_request {
529 0 0   0 1 0 my $instance = ref $_[0] eq 'App::ElasticSearch::Utilities::Connection' ? shift @_ : es_connect();
530              
531 0 0       0 $CURRENT_VERSION = _get_es_version() if !defined $CURRENT_VERSION;
532              
533 0         0 my($url,$options,$body) = _fix_version_request(@_);
534              
535             # Normalize the options
536 0   0     0 $options->{method} ||= 'GET';
537 0         0 $options->{command} = $url;
538 0         0 my $index;
539              
540 0 0       0 if( exists $options->{index} ) {
541 0 0       0 if( my $index_in = delete $options->{index} ) {
542             # No need to validate _all
543 0 0       0 if( $index_in eq '_all') {
544 0         0 $index = $index_in;
545             }
546             else {
547             # Validate each included index
548 0 0       0 my @indexes = is_arrayref($index_in) ? @{ $index_in } : split /\,/, $index_in;
  0         0  
549 0         0 $index = join(',', @indexes);
550             }
551             }
552             }
553              
554             # For the cat api, index goes *after* the command
555 0 0 0     0 if( $url =~ /^_(cat|stats)/ && $index ) {
    0          
556 0         0 $url =~ s/\/$//;
557 0         0 $url = join('/', $url, $index);
558 0         0 delete $options->{command};
559             }
560             elsif( $index ) {
561 0         0 $options->{index} = $index;
562             }
563             else {
564 0         0 $index = '';
565             }
566              
567             # Figure out if we're modifying things
568             my $modification = $url eq '_search' && $options->{method} eq 'POST' ? 0
569 0 0 0     0 : $options->{method} ne 'GET';
570              
571 0 0       0 if($modification) {
572             # Set NOOP if necessary
573 0 0 0     0 if(!$DEF{NOOP} && $DEF{MASTERONLY}) {
574 0 0       0 if( !es_master() ) {
575 0         0 $DEF{NOOP} = 1;
576             }
577             }
578              
579             # Check for noop
580 0 0       0 if( $DEF{NOOP} ) {
581 0 0 0     0 my $flag = $DEF{MASTERONLY} && !es_master() ? '--master-only' : '--noop';
582 0         0 output({color=>'cyan'}, "Called es_request($index/$options->{command}), but $flag set and method is $options->{method}");
583 0         0 return;
584             }
585             }
586              
587             # Make the request
588 0         0 my $resp = $instance->request($url,$options,$body);
589              
590             # Check the response is defined, bail if it's not
591 0 0       0 die "Unsupported request method: $options->{method}" unless defined $resp;
592              
593             # Logging
594             verbose({color=>'yellow'}, sprintf "es_request(%s/%s) returned HTTP Status %s",
595 0 0       0 $index, $options->{command}, $resp->message,
596             ) if $resp->code != 200;
597              
598             # Error handling
599 0 0 0     0 if( !$resp->is_success ) {
    0          
600 0         0 my $msg;
601             eval {
602 0         0 my @causes = ();
603 0         0 foreach my $cause ( @{ $resp->content->{error}{root_cause} } ) {
  0         0  
604 0 0       0 push @causes, $cause->{index} ? "$cause->{index}: $cause->{reason}" : $cause->{reason};
605             }
606 0         0 $msg = join("\n", map { "\t$_" } @causes);
  0         0  
607 0         0 1;
608 0 0       0 } or do {
609             # Default to the message, though it's usually unhelpful
610 0         0 $msg = $resp->{message};
611             };
612             die sprintf "es_request(%s/%s) failed[%d]:\n%s",
613 0   0     0 $index, $options->{command}, $resp->code, $msg || 'missing error message';
614              
615             } elsif( !defined $resp->content || ( !is_ref($resp->content) && !length $resp->content )) {
616             output({color=>'yellow',stderr=>1},
617             sprintf "es_request(%s/%s) empty response[%d]: %s",
618 0         0 $index, $options->{command}, $resp->code, $resp->message
619             );
620             }
621              
622 0         0 return $resp->content;
623             }
624              
625              
626              
627             my %_nodes;
628             sub es_nodes {
629 0 0   0 1 0 if(!keys %_nodes) {
630 0         0 my $res = es_request('_cluster/state/nodes', {});
631 0 0       0 if( !defined $res ) {
632 0         0 output({color=>"red"}, "es_nodes(): Unable to locate nodes in status!");
633 0         0 exit 1;
634             }
635 0         0 foreach my $id ( keys %{ $res->{nodes} } ) {
  0         0  
636 0         0 $_nodes{$id} = $res->{nodes}{$id}{name};
637             }
638             }
639              
640 0 0       0 return wantarray ? %_nodes : { %_nodes };
641             }
642              
643              
644             my $_indices_meta;
645             sub es_indices_meta {
646 0 0   0 1 0 if(!defined $_indices_meta) {
647 0         0 my $result = es_request('_cluster/state/metadata');
648 0 0       0 if ( !defined $result ) {
649 0         0 output({stderr=>1,color=>"red"}, "es_indices_meta(): Unable to locate indices in status!");
650 0         0 exit 1;
651             }
652 0         0 $_indices_meta = $result->{metadata}{indices};
653             }
654              
655 0         0 my %copy = %{ $_indices_meta };
  0         0  
656 0 0       0 return wantarray ? %copy : \%copy;
657             }
658              
659              
660             my %_valid_index = ();
661             sub es_indices {
662 0     0 1 0 my %args = (
663             state => 'open',
664             check_state => 1,
665             check_dates => 1,
666             @_
667             );
668              
669 0 0       0 es_utils_initialize() unless keys %DEF;
670              
671             # Seriously, English? Do you speak it motherfucker?
672 0 0       0 $args{state} = 'close' if $args{state} eq 'closed';
673              
674 0         0 my @indices = ();
675 0         0 my %idx = ();
676 0 0 0     0 my $wildcard = !exists $args{_all} && defined $DEF{BASE} ? sprintf "/*%s*", $DEF{BASE} : '';
677              
678             # Simplest case, single index
679 0 0 0     0 if( defined $DEF{INDEX} ) {
    0 0        
680 0         0 push @indices, $DEF{INDEX};
681             }
682             # Next simplest case, open indexes
683             elsif( !exists $args{_all} && $args{check_state} && $args{state} eq 'open' ) {
684             # Use _stats because it's break neck fast
685 0 0       0 if( my $res = es_request($wildcard . '/_stats/docs') ) {
686 0         0 foreach my $idx ( keys %{ $res->{indices} } ) {
  0         0  
687 0         0 $idx{$idx} = 'open';
688             }
689             }
690             }
691             else {
692 0         0 my $res = es_request('_cat/indices' . $wildcard, { uri_param => { h => 'index,status' } });
693 0         0 foreach my $entry (@{ $res }) {
  0         0  
694 0 0       0 my ($index,$status) = is_hashref($entry) ? @{ $entry }{qw(index status)} : split /\s+/, $entry;
  0         0  
695 0         0 $idx{$index} = $status;
696             }
697             }
698              
699 0         0 foreach my $index (sort keys %idx) {
700 0 0       0 if(!exists $args{_all}) {
701 0         0 my $status = $idx{$index};
702             # State Check Disqualification
703 0 0 0     0 if($args{state} ne 'all' && $args{check_state}) {
704 0         0 my $result = $status eq $args{state};
705 0 0       0 next unless $result;
706             }
707              
708 0         0 my $p = es_pattern();
709 0 0       0 next unless $index =~ /$p->{re}/;
710 0         0 debug({indent=>2},"= name checks succeeded");
711              
712 0 0 0     0 if ($args{older} && defined $DEF{DAYS}) {
    0 0        
713 0         0 my $days_old = es_index_days_old( $index );
714 0 0 0     0 if (!defined $days_old || $days_old < $DEF{DAYS}) {
715 0         0 next;
716             }
717             }
718             elsif( $args{check_dates} && defined $DEF{DAYS} ) {
719              
720 0         0 my $days_old = es_index_days_old( $index );
721 0 0 0     0 if( !defined $days_old ) {
    0          
722 0         0 debug({indent=>2,color=>'red'}, "! error locating date in string, skipping !");
723 0         0 next;
724             }
725             elsif( $DEF{DAYS} >= 0 && $days_old >= $DEF{DAYS} ) {
726 0         0 next;
727             }
728             }
729             }
730             else {
731 0         0 debug({indent=>1}, "Called with _all, all checks skipped.");
732             }
733 0         0 debug({indent=>1,color=>"green"}, "+ match!");
734 0         0 push @indices, $index;
735             }
736              
737             # We retrieved these from the cluster, so preserve them here.
738 0         0 $_valid_index{$_} = 1 for @indices;
739              
740 0 0       0 return wantarray ? @indices : \@indices;
741             }
742              
743              
744             sub es_index_strip_date {
745 48     48 1 95 my ($index) = @_;
746              
747 48 50       90 return -1 unless defined $index;
748              
749 48 50       105 es_utils_initialize() unless keys %DEF;
750              
751 48 50       350 if( $index =~ s/[-_]$PATTERN_REGEX{DATE}.*// ) {
752 48         152 return $index;
753             }
754 0         0 return;
755             }
756              
757              
758             my %_stripped=();
759              
760             sub es_index_bases {
761 24     24 1 23122 my ($index) = @_;
762              
763 24 50       64 return unless defined $index;
764              
765             # Strip to the base
766 24         50 my $stripped = es_index_strip_date($index);
767 24 50 33     109 return unless defined $stripped and length $stripped;
768              
769             # Compute if we haven't already memoized
770 24 100       75 if( !exists $_stripped{$stripped} ) {
771 3         8 my %bases=();
772 3 50       13 my @parts = grep { defined && length } split /[-_]/, $stripped;
  5         25  
773 3         35 debug(sprintf "es_index_bases(%s) dissected to %s", $index, join(',', @parts));
774 3 100       41 my $sep = index( $stripped, '_' ) >= 0 ? '_' : '-';
775              
776 3         7 my %collected = ();
777 3         11 foreach my $end ( 0..$#parts ) {
778 5         16 my $name = join($sep, @parts[0..$end]);
779 5         16 $collected{$name} = 1;
780             }
781 3         17 $_stripped{$stripped} = [ sort keys %collected ]
782             }
783              
784 24         42 return @{ $_stripped{$stripped} };
  24         91  
785             }
786              
787              
788             my $NOW = timegm(0,0,0,(gmtime)[3,4,5]);
789             sub es_index_days_old {
790 24     24 1 48 my ($index) = @_;
791              
792 24 50       47 return unless defined $index;
793              
794 24 50       52 es_utils_initialize() unless keys %DEF;
795              
796 24 50       181 if( my ($dateStr) = ($index =~ /($PATTERN_REGEX{DATE})/) ) {
797 24         46 my @date=();
798 24 50       59 if(length $DEF{DATESEP}) {
799 24         97 @date = reverse map { int } split /\Q$DEF{DATESEP}\E/, $dateStr;
  72         170  
800             }
801             else {
802 0         0 for my $len (qw(4 2 2)) {
803 0         0 unshift @date, substr($dateStr,0,$len,'');
804             }
805             }
806 24         60 $date[1]--; # move 1-12 -> 0-11
807 24         39 my $idx_time = eval { timegm( 0,0,0, @date ) };
  24         83  
808 24 50       762 return unless $idx_time;
809 24         40 my $diff = $NOW - $idx_time;
810 24         37 $diff++; # Add one second
811 24         280 debug({color=>"yellow"}, sprintf "es_index_days_old(%s) - Time difference is %0.3f", $index, $diff/86400);
812 24         311 return int($diff / 86400);
813             }
814 0         0 return;
815             }
816              
817              
818              
819             sub es_index_shards {
820 0     0 1 0 my ($index) = @_;
821              
822 0         0 my %shards = map { $_ => 0 } qw(primaries replicas);
  0         0  
823 0         0 my $result = es_request('_settings', {index=>$index});
824 0 0 0     0 if( defined $result && is_hashref($result) ) {
825 0         0 $shards{primaries} = $result->{$index}{settings}{index}{number_of_shards};
826 0         0 $shards{replicas} = $result->{$index}{settings}{index}{number_of_replicas};
827             }
828              
829 0 0       0 return wantarray ? %shards : \%shards;
830             }
831              
832              
833             sub es_index_valid {
834 0     0 1 0 my ($index) = @_;
835              
836 0 0 0     0 return unless defined $index && length $index;
837 0 0       0 return $_valid_index{$index} if exists $_valid_index{$index};
838              
839 0         0 my $es = es_connect();
840 0         0 my $result;
841 0         0 eval {
842 0         0 debug("Running index_exists");
843 0         0 $result = $es->exists( index => $index );
844             };
845 0         0 return $_valid_index{$index} = $result;
846             }
847              
848              
849             sub es_index_fields {
850 0     0 1 0 my ($index) = @_;
851              
852 0         0 my $result = es_request('_mapping', { index => $index });
853              
854 0 0       0 return unless defined $result;
855              
856 0         0 my %fields;
857 0         0 foreach my $idx ( sort keys %{ $result } ) {
  0         0  
858             # Handle Version incompatibilities
859 0 0       0 my $ref = exists $result->{$idx}{mappings} ? $result->{$idx}{mappings} : $result->{$idx};
860              
861             # Loop through the mappings, skipping _default_, except on 7.x where we notice "properties"
862             my @mappings = exists $ref->{properties} ? ($ref)
863 0 0       0 : map { $ref->{$_} } grep { $_ ne '_default_' } keys %{ $ref };
  0         0  
  0         0  
  0         0  
864 0         0 foreach my $mapping (@mappings) {
865 0         0 _find_fields(\%fields,$mapping);
866             }
867             }
868             # Return the results
869 0         0 return \%fields;
870             }
871              
872             {
873             # Closure for field metadata
874             my $nested_path;
875              
876             sub _add_fields {
877 0     0   0 my ($f,$type,@path) = @_;
878              
879 0 0       0 return unless @path;
880              
881 0         0 my %i = (
882             type => $type,
883             );
884              
885             # Store the full path
886 0         0 my $key = join('.', @path);
887              
888 0 0       0 if( $nested_path ) {
889 0         0 $i{nested_path} = $nested_path;
890 0         0 $i{nested_key} = substr( $key, length($nested_path)+1 );
891             }
892              
893 0         0 $f->{$key} = \%i;
894             }
895              
896             sub _find_fields {
897 0     0   0 my ($f,$ref,@path) = @_;
898              
899 0 0       0 return unless is_hashref($ref);
900             # Handle things with properties
901 0 0 0     0 if( exists $ref->{properties} && is_hashref($ref->{properties}) ) {
    0          
902 0 0 0     0 $nested_path = join('.', @path) if $ref->{type} and $ref->{type} eq 'nested';
903 0         0 foreach my $k (sort keys %{ $ref->{properties} }) {
  0         0  
904 0         0 _find_fields($f,$ref->{properties}{$k},@path,$k);
905             }
906 0         0 undef($nested_path);
907             }
908             # Handle elements that contain data
909             elsif( exists $ref->{type} ) {
910 0         0 _add_fields($f,$ref->{type},@path);
911             # Handle multifields
912 0 0 0     0 if( exists $ref->{fields} && is_hashref($ref->{fields}) ) {
913 0         0 foreach my $k (sort keys %{ $ref->{fields} } ) {
  0         0  
914 0         0 _add_fields($f,$ref->{type},@path,$k);
915             }
916             }
917             }
918             # Unknown data, throw an error if we care that deeply.
919             else {
920             debug({stderr=>1,color=>'red'},
921             sprintf "_find_fields(): Invalid property at: %s ref info: %s",
922             join('.', @path),
923 0 0       0 join(',', is_hashref($ref) ? sort keys %{$ref} :
  0 0       0  
924             ref $ref ? ref $ref : 'unknown ref'
925             ),
926             );
927             }
928             }
929             }
930              
931              
932             sub es_close_index {
933 0     0 1 0 my($index) = @_;
934              
935 0         0 return es_request('_close',{ method => 'POST', index => $index });
936             }
937              
938              
939             sub es_open_index {
940 0     0 1 0 my($index) = @_;
941              
942 0         0 return es_request('_open',{ method => 'POST', index => $index });
943             }
944              
945              
946             sub es_delete_index {
947 0     0 1 0 my($index) = @_;
948              
949 0         0 return es_request('',{ method => 'DELETE', index => $index });
950             }
951              
952              
953             sub es_optimize_index {
954 0     0 1 0 my($index) = @_;
955              
956 0         0 return es_request('_forcemerge',{
957             method => 'POST',
958             index => $index,
959             uri_param => {
960             max_num_segments => 1,
961             },
962             });
963             }
964              
965              
966             sub es_apply_index_settings {
967 0     0 1 0 my($index,$settings) = @_;
968              
969 0 0       0 if(!is_hashref($settings)) {
970 0         0 output({stderr=>1,color=>'red'}, 'usage is es_apply_index_settings($index,$settings_hashref)');
971 0         0 return;
972             }
973              
974 0         0 return es_request('_settings',{ method => 'PUT', index => $index },$settings);
975             }
976              
977              
978             sub es_index_segments {
979 0     0 1 0 my ($index) = @_;
980              
981 0 0 0     0 if( !defined $index || !length $index || !es_index_valid($index) ) {
      0        
982 0         0 output({stderr=>1,color=>'red'}, "es_index_segments('$index'): invalid index");
983 0         0 return;
984             }
985              
986 0         0 return es_request('_segments', {
987             index => $index,
988             });
989              
990             }
991              
992              
993             sub es_segment_stats {
994 0     0 1 0 my ($index) = @_;
995              
996 0         0 my %segments = map { $_ => 0 } qw(shards segments);
  0         0  
997 0         0 my $result = es_index_segments($index);
998              
999 0 0       0 if(defined $result) {
1000 0         0 my $shard_data = $result->{indices}{$index}{shards};
1001 0         0 foreach my $id (keys %{$shard_data}) {
  0         0  
1002 0         0 $segments{segments} += $shard_data->{$id}[0]{num_search_segments};
1003 0         0 $segments{shards}++;
1004             }
1005             }
1006 0 0       0 return wantarray ? %segments : \%segments;
1007             }
1008              
1009              
1010              
1011             sub es_index_stats {
1012 0     0 1 0 my ($index) = @_;
1013              
1014 0         0 return es_request('_stats', {
1015             index => $index
1016             });
1017             }
1018              
1019              
1020              
1021             sub es_settings {
1022 0     0 1 0 return es_request('_settings');
1023             }
1024              
1025              
1026             sub es_node_stats {
1027 0     0 1 0 my (@nodes) = @_;
1028              
1029 0         0 my @cmd = qw(_nodes);
1030 0 0       0 push @cmd, join(',', @nodes) if @nodes;
1031 0         0 push @cmd, 'stats';
1032              
1033 0         0 return es_request(join('/',@cmd));
1034             }
1035              
1036              
1037             sub es_flatten_hash {
1038 2     2 1 1109 my $hash = shift;
1039 2         17 my $_flat = flatten($hash, { HashDelimiter=>':', ArrayDelimiter=>':' });
1040 2         705 my %compat = map { s/:/./gr => $_flat->{$_} } keys %{ $_flat };
  6         22  
  2         7  
1041 2         8 return \%compat;
1042             }
1043              
1044              
1045             sub def {
1046 0     0 1   my($key)= map { uc }@_;
  0            
1047              
1048 0 0         es_utils_initialize() unless keys %DEF;
1049              
1050 0 0         return exists $DEF{$key} ? $DEF{$key} : undef;
1051             }
1052              
1053              
1054             sub es_local_index_meta {
1055 0     0 1   my ($key,$name_or_base) = @_;
1056              
1057 0 0         es_utils_initialize() unless keys %DEF;
1058              
1059 0 0         if( exists $_GLOBALS{meta} ) {
1060 0           my $meta = $_GLOBALS{meta};
1061 0           my @search = ( $name_or_base );
1062 0           push @search, es_index_strip_date( $name_or_base );
1063 0           push @search, es_index_bases($name_or_base);
1064              
1065 0           foreach my $check ( @search ) {
1066 0 0 0       if( exists $meta->{$check} && exists $meta->{$check}{$key} ) {
1067 0           return $meta->{$check}{$key};
1068             }
1069             }
1070             }
1071              
1072 0           return;
1073             }
1074              
1075              
1076              
1077             1;
1078              
1079             __END__
1080              
1081             =pod
1082              
1083             =head1 NAME
1084              
1085             App::ElasticSearch::Utilities - Utilities for Monitoring ElasticSearch
1086              
1087             =head1 VERSION
1088              
1089             version 8.5
1090              
1091             =head1 SYNOPSIS
1092              
1093             This library contains utilities for unified interfaces in the scripts.
1094              
1095             This a set of utilities to make monitoring ElasticSearch clusters much simpler.
1096              
1097             Included are:
1098              
1099             B<SEARCHING>:
1100              
1101             scripts/es-search.pl - Utility to interact with LogStash style indices from the CLI
1102              
1103             B<MONITORING>:
1104              
1105             scripts/es-graphite-dynamic.pl - Perform index maintenance on daily indexes
1106             scripts/es-status.pl - Command line utility for ES Metrics
1107             scripts/es-storage-overview.pl - View how shards/data is aligned on your cluster
1108             scripts/es-nodes.pl - View node information
1109              
1110             B<MAINTENANCE>:
1111              
1112             scripts/es-daily-index-maintenance.pl - Perform index maintenance on daily indexes
1113             scripts/es-alias-manager.pl - Manage index aliases automatically
1114             scripts/es-open.pl - Open any closed indices matching a index parameters
1115              
1116             B<MANAGEMENT>:
1117              
1118             scripts/es-apply-settings.pl - Apply settings to all indexes matching a pattern
1119             scripts/es-cluster-settings.pl - Manage cluster settings
1120             scripts/es-copy-index.pl - Copy an index from one cluster to another
1121             scripts/es-storage-overview.pl - View how shards/data is aligned on your cluster
1122              
1123             B<DEPRECATED>:
1124              
1125             scripts/es-graphite-static.pl - Send ES Metrics to Graphite or Cacti
1126              
1127             The App::ElasticSearch::Utilities module simply serves as a wrapper around the scripts for packaging and
1128             distribution.
1129              
1130             =head1 FUNCTIONS
1131              
1132             =head2 es_utils_initialize()
1133              
1134             Takes an optional reference to an C<@ARGV> like array. Performs environment and
1135             argument parsing.
1136              
1137             =head2 es_globals($key)
1138              
1139             Grab the value of the global value from the es-utils.yaml files.
1140              
1141             =head2 es_basic_auth($host)
1142              
1143             Get the user/password combination for this host. This is called from LWP::UserAgent if
1144             it recieves a 401, so the auth condition must be satisfied.
1145              
1146             Returns the username and password as a list.
1147              
1148             =head2 es_pass_exec(host, username)
1149              
1150             Called from es_basic_auth to exec a program, capture the password
1151             and return it to the caller. This allows the use of password vaults
1152             and keychains.
1153              
1154             =head2 es_pattern
1155              
1156             Returns a hashref of the pattern filter used to get the indexes
1157             {
1158             string => '*',
1159             re => '.*',
1160             }
1161              
1162             =head2 es_connect
1163              
1164             Without options, this connects to the server defined in the args. If passed
1165             an array ref, it will use that as the connection definition.
1166              
1167             =head2 es_master([$handle])
1168              
1169             Returns true (1) if the handle is to the the cluster master, or false (0) otherwise.
1170              
1171             =head2 es_request([$handle],$command,{ method => 'GET', uri_param => { a => 1 } }, {})
1172              
1173             Retrieve URL from ElasticSearch, returns a hash reference
1174              
1175             First hash ref contains options, including:
1176              
1177             uri_param Query String Parameters
1178             index Index name
1179             type Index type
1180             method Default is GET
1181              
1182             If the request is not successful, this function will throw a fatal exception.
1183             If you'd like to proceed you need to catch that error.
1184              
1185             =head2 es_nodes
1186              
1187             Returns the hash of index meta data.
1188              
1189             =head2 es_indices_meta
1190              
1191             Returns the hash of index meta data.
1192              
1193             =head2 es_indices
1194              
1195             Returns a list of active indexes matching the filter criteria specified on the command
1196             line. Can handle indices named:
1197              
1198             logstash-YYYY.MM.DD
1199             dcid-logstash-YYYY.MM.DD
1200             logstash-dcid-YYYY.MM.DD
1201             logstash-YYYY.MM.DD-dcid
1202              
1203             Makes use of --datesep to determine where the date is.
1204              
1205             Options include:
1206              
1207             =over 4
1208              
1209             =item B<state>
1210              
1211             Default is 'open', can be used to find 'closed' indexes as well.
1212              
1213             =item B<check_state>
1214              
1215             Default is 1, set to 0 to disable state checks. The combination of the default
1216             with this option and the default for B<state> means only open indices are returned.
1217              
1218             =item B<check_dates>
1219              
1220             Default is 1, set to 0 to disable checking index age.
1221              
1222             =back
1223              
1224             =head2 es_index_strip_date( 'index-name' )
1225              
1226             Returns the index name with the date removed.
1227              
1228             =head2 es_index_bases( 'index-name' )
1229              
1230             Returns an array of the possible index base names for this index
1231              
1232             =head2 es_index_days_old( 'index-name' )
1233              
1234             Return the number of days old this index is.
1235              
1236             =head2 es_index_shards( 'index-name' )
1237              
1238             Returns the number of replicas for a given index.
1239              
1240             =head2 es_index_valid( 'index-name' )
1241              
1242             Checks if the specified index is valid
1243              
1244             =head2 es_index_fields('index-name')
1245              
1246             Returns a hash reference with the following data:
1247              
1248             key_name:
1249             type: field_data_type
1250             # If the field is nested
1251             nested_path: nested_path
1252             nested_key: nested_key
1253              
1254             =head2 es_close_index('index-name')
1255              
1256             Closes an index
1257              
1258             =head2 es_open_index('index-name')
1259              
1260             Open an index
1261              
1262             =head2 es_delete_index('index-name')
1263              
1264             Deletes an index
1265              
1266             =head2 es_optimize_index('index-name')
1267              
1268             Optimize an index to a single segment per shard
1269              
1270             =head2 es_apply_index_settings('index-name', { settings })
1271              
1272             Apply a HASH of settings to an index.
1273              
1274             =head2 es_index_segments( 'index-name' )
1275              
1276             Exposes GET /$index/_segments
1277              
1278             Returns the segment data from the index in hashref:
1279              
1280             =head2 es_segment_stats($index)
1281              
1282             Return the number of shards and segments in an index as a hashref
1283              
1284             =head2 es_index_stats( 'index-name' )
1285              
1286             Exposes GET /$index/_stats
1287              
1288             Returns a hashref
1289              
1290             =head2 es_settings()
1291              
1292             Exposes GET /_settings
1293              
1294             Returns a hashref
1295              
1296             =head2 es_node_stats()
1297              
1298             Exposes GET /_nodes/stats
1299              
1300             Returns a hashref
1301              
1302             =head2 es_flatten_hash
1303              
1304             Performs flattening that's compatible with Elasticsearch's flattening.
1305              
1306             =head2 def('key')
1307              
1308             Exposes Definitions grabbed by options parsing
1309              
1310             =head2 es_local_index_meta(key => 'base' || 'index')
1311              
1312             Fetch meta-data from the local config file, i.e. C<~/.es-utils.yaml>.
1313              
1314             Format is:
1315              
1316             ---
1317             meta:
1318             index_name:
1319             key: value
1320             index_basename:
1321             key: value
1322              
1323             The most specific version is searched first, followed by the index stripped of
1324             it's date, and then on through all the bases discovered with
1325             C<es_index_bases()>.
1326              
1327             This is used by the C<es-search.pl> utility to do lookups of the B<timestamp>
1328             field it needs to sort documents, i.e.:
1329              
1330             ---
1331             meta:
1332             logstash:
1333             timestamp: '@timestamp'
1334             host: es-cluster-01.int.example.com
1335             bro:
1336             timestamp: 'timestamp'
1337              
1338             =head1 ARGS
1339              
1340             From App::ElasticSearch::Utilities:
1341              
1342             --local Use localhost as the elasticsearch host
1343             --host ElasticSearch host to connect to
1344             --port HTTP port for your cluster
1345             --proto Defaults to 'http', can also be 'https'
1346             --http-username HTTP Basic Auth username
1347             --password-exec Script to run to get the users password
1348             --insecure Don't verify TLS certificates
1349             --cacert Specify the TLS CA file
1350             --capath Specify the directory with TLS CAs
1351             --cert Specify the path to the client certificate
1352             --key Specify the path to the client private key file
1353             --noop Any operations other than GET are disabled, can be negated with --no-noop
1354             --timeout Timeout to ElasticSearch, default 10
1355             --keep-proxy Do not remove any proxy settings from %ENV
1356             --index Index to run commands against
1357             --base For daily indexes, reference only those starting with "logstash"
1358             (same as --pattern logstash-* or logstash-DATE)
1359             --datesep Date separator, default '.' also (--date-separator)
1360             --pattern Use a pattern to operate on the indexes
1361             --days If using a pattern or base, how many days back to go, default: 1
1362              
1363             See also the "CONNECTION ARGUMENTS" and "INDEX SELECTION ARGUMENTS" sections from App::ElasticSearch::Utilities.
1364              
1365             =head1 ARGUMENT GLOBALS
1366              
1367             Some options may be specified in the B</etc/es-utils.yaml>, B<$HOME/.es-utils.yaml>
1368             or B<$HOME/.config/es-utils/config.yaml> file:
1369              
1370             ---
1371             base: logstash
1372             days: 7
1373             host: esproxy.example.com
1374             port: 80
1375             timeout: 10
1376             proto: https
1377             http-username: bob
1378             password-exec: /home/bob/bin/get-es-passwd.sh
1379              
1380             =head1 CONNECTION ARGUMENTS
1381              
1382             Arguments for establishing a connection with the cluster. Unless specified otherwise, these options
1383             can all be set in the globals file.
1384              
1385             =over
1386              
1387             =item B<local>
1388              
1389             Assume ElasticSearch is running locally, connect to localhost.
1390              
1391             =item B<host>
1392              
1393             Use a different hostname or IP address to connect.
1394              
1395             =item B<port>
1396              
1397             Defaults to 9200.
1398              
1399             =item B<proto>
1400              
1401             Defaults to 'http', can also be 'https'.
1402              
1403             =item B<http-username>
1404              
1405             If HTTP Basic Authentication is required, use this username.
1406              
1407             See also the L<HTTP Basic Authentication> section for more details
1408              
1409             =item B<password-exec>
1410              
1411             If HTTP Basic Authentication is required, run this command, passing the arguments:
1412              
1413             <command_to_run> <es_host> <es_username>
1414              
1415             The script expects the last line to contain the password in plaintext.
1416              
1417             =item B<noop>
1418              
1419             Prevents any communication to the cluster from making changes to the settings or data contained therein.
1420             In short, it prevents anything but HEAD and GET requests, B<except> POST requests to the _search endpoint.
1421              
1422             =item B<timeout>
1423              
1424             Timeout for connections and requests, defaults to 10.
1425              
1426             =item B<keep-proxy>
1427              
1428             By default, HTTP proxy environment variables are stripped. Use this option to keep your proxy environment variables
1429             in tact.
1430              
1431             =item B<insecure>
1432              
1433             Don't verify TLS certificates
1434              
1435             =item B<cacert>
1436              
1437             Specify a file with the TLS CA certificates.
1438              
1439             =item B<capath>
1440              
1441             Specify a directory containing the TLS CA certificates.
1442              
1443             =item B<cert>
1444              
1445             Specify the path to the TLS client certificate file..
1446              
1447             =item B<key>
1448              
1449             Specify the path to the TLS client private key file.
1450              
1451             =back
1452              
1453             =head1 INDEX SELECTION ARGUMENTS
1454              
1455             =over
1456              
1457             =item B<base>
1458              
1459             In an environment using monthly, weekly, daily, or hourly indexes. The base index name is everything without the date.
1460             Parsing for bases, also provides splitting and matching on segments of the index name delineated by the '-' character.
1461             If we have the following indexes:
1462              
1463             web-dc1-YYYY.MM.DD
1464             web-dc2-YYYY.MM.DD
1465             logstash-dc1-YYYY.MM.DD
1466             logstash-dc2-YYYY.MM.DD
1467              
1468             Valid bases would be:
1469              
1470             web
1471             web-dc1
1472             web-dc2
1473             logstash
1474             logstash-dc1
1475             logstash-dc2
1476             dc1
1477             dc2
1478              
1479             Combining that with the days option can provide a way to select many indexes at once.
1480              
1481             =item B<days>
1482              
1483             How many days backwards you want your operation to be relevant.
1484              
1485             =item B<datesep>
1486              
1487             Default is '.' Can be set to an empty string for no separator.
1488              
1489             =item B<pattern>
1490              
1491             A pattern to match the indexes. Can expand the following key words and characters:
1492              
1493             '*' expanded to '.*'
1494             'ANY' expanded to '.*'
1495             'DATE' expanded to a pattern to match a date,
1496              
1497             The indexes are compared against this pattern.
1498              
1499             =back
1500              
1501             =head1 HTTP Basic Authentication
1502              
1503             HTTP Basic Authorization is only supported when the C<proto> is set to B<https>
1504             as not to leak credentials all over.
1505              
1506             The username is selected by going through these mechanisms until one is found:
1507              
1508             --http-username
1509             'http-username' in /etc/es-utils.yml or ~/.es-utils.yml
1510             Netrc element matching the hostname of the request
1511             CLI::Helpers prompt()
1512              
1513             Once the username has been resolved, the following mechanisms are tried in order:
1514              
1515             Netrc element matching the hostname of the request
1516             Password executable defined by --password-exec
1517             'password-exec' in /etc/es-utils.yml, ~/.es-utils.yml
1518             CLI::Helpers prompt()
1519              
1520             =head2 Password Exec
1521              
1522             It is B<BAD> practice to specify passwords as a command line argument, or store it in a plaintext
1523             file. There are cases where this may be necessary, but it is not recommended. The best method for securing your
1524             password is to use the B<password-exec> option.
1525              
1526             This option must point to an executable script. That script will be passed two arguments, the hostname and the username
1527             for the request. It expects the password printed to STDOUT as the last line of output. Here's an example password-exec setup
1528             using Apple Keychain:
1529              
1530             #!/bin/sh
1531              
1532             HOSTNAME=$1;
1533             USERNAME=$2;
1534              
1535             /usr/bin/security find-generic-password -w -a "$USERNAME" -s "$HOSTNAME"
1536              
1537             If we save this to "$HOME/bin/get-passwd.sh" we can execute a script
1538             like this:
1539              
1540             $ es-search.pl --http-username bob --password-exec $HOME/bin/get-passwd.sh \
1541             --base secure-data --fields
1542              
1543             Though it's probably best to set this in your ~/.es-utils.yml file:
1544              
1545             ---
1546             host: secured-cluster.example.org
1547             port: 443
1548             proto: https
1549             http-username: bob
1550             password-exec: /home/bob/bin/get-passwd.sh
1551              
1552             =head3 CLI::Helpers and Password Prompting
1553              
1554             If all the fails to yield a password, the last resort is to use CLI::Helpers::prompt() to ask the user for their
1555             password. If the user is using version 1.1 or higher of CLI::Helpers, this call will turn off echo and readline magic
1556             for the password prompt.
1557              
1558             =head1 INSTALL
1559              
1560             B<This library attempts to provide scripts compatible with version 0.19 through 1.1 of ElasticSearch>.
1561              
1562             Recommended install with L<CPAN Minus|http://cpanmin.us>:
1563              
1564             cpanm App::ElasticSearch::Utilities
1565              
1566             You can also use CPAN:
1567              
1568             cpan App::ElasticSearch::Utilities
1569              
1570             Or if you'd prefer to manually install:
1571              
1572             export RELEASE=<CurrentRelease>
1573              
1574             wget "https://github.com/reyjrar/es-utils/blob/master/releases/App-ElasticSearch-Utilities-$RELEASE.tar.gz?raw=true" -O es-utils.tgz
1575              
1576             tar -zxvf es-utils.tgz
1577              
1578             cd App-ElasticSearch-Utilities-$RELEASE
1579              
1580             perl Makefile.PL
1581              
1582             make
1583              
1584             make install
1585              
1586             This will take care of ensuring all the dependencies are satisfied and will install the scripts into the same
1587             directory as your Perl executable.
1588              
1589             =head2 USAGE
1590              
1591             The tools are all wrapped in their own documentation, please see:
1592              
1593             $UTILITY --help
1594             $UTILITY --manual
1595              
1596             For individual options and capabilities
1597              
1598             =head2 PATTERNS
1599              
1600             Patterns are used to match an index to the aliases it should have. A few symbols are expanded into
1601             regular expressions. Those patterns are:
1602              
1603             * expands to match any number of any characters.
1604             DATE expands to match YYYY.MM.DD, YYYY-MM-DD, or YYYYMMDD
1605             ANY expands to match any number of any characters.
1606              
1607             =head1 AUTHOR
1608              
1609             Brad Lhotsky <brad@divisionbyzero.net>
1610              
1611             =head1 CONTRIBUTORS
1612              
1613             =for stopwords Alexey Shatlovsky Samit Badle Takumi Sakamoto Vitaly Shupak Surikov Andrei Grechkin Daniel Ostermeier Jason Rojas Kang-min Liu Lisa Hare Markus Linnala Matthew Feinberg Mohammad S Anwar
1614              
1615             =over 4
1616              
1617             =item *
1618              
1619             Alexey Shatlovsky <alexey.shatlovsky@booking.com>
1620              
1621             =item *
1622              
1623             Samit Badle <Samit.Badle@gmail.com>
1624              
1625             =item *
1626              
1627             Takumi Sakamoto <takumi.saka@gmail.com>
1628              
1629             =item *
1630              
1631             Vitaly Shupak <vitaly.shupak@deshaw.com>
1632              
1633             =item *
1634              
1635             Alexey Surikov <ksurent@gmail.com>
1636              
1637             =item *
1638              
1639             Andrei Grechkin <andrei.grechkin@booking.com>
1640              
1641             =item *
1642              
1643             Daniel Ostermeier <daniel.ostermeier@gmail.com>
1644              
1645             =item *
1646              
1647             Jason Rojas <jason.rojas@mgo.com>
1648              
1649             =item *
1650              
1651             Kang-min Liu <gugod@gugod.org>
1652              
1653             =item *
1654              
1655             Lisa Hare <lhare@inview.co.uk>
1656              
1657             =item *
1658              
1659             Markus Linnala <Markus.Linnala@cybercom.com>
1660              
1661             =item *
1662              
1663             Matthew Feinberg <mattf@intex.com>
1664              
1665             =item *
1666              
1667             Mohammad S Anwar <mohammad.anwar@yahoo.com>
1668              
1669             =back
1670              
1671             =for :stopwords cpan testmatrix url bugtracker rt cpants kwalitee diff irc mailto metadata placeholders metacpan
1672              
1673             =head1 SUPPORT
1674              
1675             =head2 Websites
1676              
1677             The following websites have more information about this module, and may be of help to you. As always,
1678             in addition to those websites please use your favorite search engine to discover more resources.
1679              
1680             =over 4
1681              
1682             =item *
1683              
1684             MetaCPAN
1685              
1686             A modern, open-source CPAN search engine, useful to view POD in HTML format.
1687              
1688             L<https://metacpan.org/release/App-ElasticSearch-Utilities>
1689              
1690             =item *
1691              
1692             CPAN Testers
1693              
1694             The CPAN Testers is a network of smoke testers who run automated tests on uploaded CPAN distributions.
1695              
1696             L<http://www.cpantesters.org/distro/A/App-ElasticSearch-Utilities>
1697              
1698             =item *
1699              
1700             CPAN Testers Matrix
1701              
1702             The CPAN Testers Matrix is a website that provides a visual overview of the test results for a distribution on various Perls/platforms.
1703              
1704             L<http://matrix.cpantesters.org/?dist=App-ElasticSearch-Utilities>
1705              
1706             =back
1707              
1708             =head2 Bugs / Feature Requests
1709              
1710             This module uses the GitHub Issue Tracker: L<https://github.com/reyjrar/es-utils/issues>
1711              
1712             =head2 Source Code
1713              
1714             This module's source code is available by visiting:
1715             L<https://github.com/reyjrar/es-utils>
1716              
1717             =head1 COPYRIGHT AND LICENSE
1718              
1719             This software is Copyright (c) 2023 by Brad Lhotsky.
1720              
1721             This is free software, licensed under:
1722              
1723             The (three-clause) BSD License
1724              
1725             =cut