Merge lp:~percona-toolkit-dev/percona-toolkit/pt-agent into lp:~percona-toolkit-dev/percona-toolkit/release-2.2.3

Proposed by Daniel Nichter
Status: Merged
Merged at revision: 584
Proposed branch: lp:~percona-toolkit-dev/percona-toolkit/pt-agent
Merge into: lp:~percona-toolkit-dev/percona-toolkit/release-2.2.3
Diff against target: 28562 lines (+25960/-1139) (has conflicts)
65 files modified
bin/pt-agent (+9343/-0)
bin/pt-query-digest (+364/-71)
lib/Cxn.pm (+1/-2)
lib/Daemon.pm (+221/-163)
lib/HTTP/Micro.pm (+332/-329)
lib/JSONReportFormatter.pm (+305/-44)
lib/Lmo.pm (+0/-379)
lib/MockSth.pm (+1/-1)
lib/MockSync.pm (+1/-1)
lib/MockSyncStream.pm (+1/-1)
lib/Percona/Agent/Logger.pm (+343/-0)
lib/Percona/Test.pm (+806/-0)
lib/Percona/Test/Mock/AgentLogger.pm (+129/-0)
lib/Percona/Test/Mock/UserAgent.pm (+71/-0)
lib/Percona/Toolkit.pm (+50/-3)
lib/Percona/WebAPI/Client.pm (+318/-0)
lib/Percona/WebAPI/Exception/Request.pm (+69/-0)
lib/Percona/WebAPI/Exception/Resource.pm (+66/-0)
lib/Percona/WebAPI/Representation.pm (+86/-0)
lib/Percona/WebAPI/Resource/Agent.pm (+77/-0)
lib/Percona/WebAPI/Resource/Config.pm (+55/-0)
lib/Percona/WebAPI/Resource/LogEntry.pm (+66/-0)
lib/Percona/WebAPI/Resource/Service.pm (+94/-0)
lib/Percona/WebAPI/Resource/Task.pm (+62/-0)
lib/PerconaTest.pm (+0/-15)
lib/QueryReportFormatter.pm (+1/-1)
lib/Safeguards.pm (+94/-0)
lib/SlowLogParser.pm (+11/-3)
lib/VersionCheck.pm (+680/-0)
lib/VersionParser.pm (+0/-2)
t/lib/Daemon.t (+67/-40)
t/lib/HTTP/Micro.t (+3/-3)
t/lib/Percona/Toolkit.t (+1/-2)
t/lib/Percona/WebAPI/Client.t (+235/-0)
t/lib/Percona/WebAPI/Representation.t (+51/-0)
t/lib/Safeguards.t (+69/-0)
t/lib/samples/daemonizes.pl (+17/-17)
t/lib/samples/slowlogs/slow057.txt (+8835/-0)
t/pt-agent/basics.t (+101/-0)
t/pt-agent/get_services.t (+423/-0)
t/pt-agent/init_agent.t (+280/-0)
t/pt-agent/make_new_crontab.t (+151/-0)
t/pt-agent/replace_special_vars.t (+73/-0)
t/pt-agent/run_agent.t (+527/-0)
t/pt-agent/run_service.t (+503/-0)
t/pt-agent/samples/crontab001.out (+2/-0)
t/pt-agent/samples/crontab002.in (+1/-0)
t/pt-agent/samples/crontab002.out (+3/-0)
t/pt-agent/samples/crontab003.in (+3/-0)
t/pt-agent/samples/crontab003.out (+3/-0)
t/pt-agent/samples/crontab004.in (+2/-0)
t/pt-agent/samples/crontab004.out (+2/-0)
t/pt-agent/samples/query-history/data001.json (+139/-0)
t/pt-agent/samples/query-history/data001.send (+153/-0)
t/pt-agent/samples/service001 (+19/-0)
t/pt-agent/samples/write_services001 (+19/-0)
t/pt-agent/schedule_services.t (+200/-0)
t/pt-agent/send_data.t (+176/-0)
t/pt-agent/write_services.t (+108/-0)
t/pt-query-digest/json.t (+3/-3)
t/pt-query-digest/resume.t (+38/-31)
t/pt-query-digest/samples/empty_report.txt (+0/-2)
t/pt-query-digest/samples/output_json_slow002.txt (+31/-2)
t/pt-query-digest/samples/output_json_tcpdump021.txt (+74/-24)
util/check-dev-env (+1/-0)
Text conflict in lib/Percona/Toolkit.pm
Conflict adding file lib/VersionCheck.pm.  Moved existing file to lib/VersionCheck.pm.moved.
To merge this branch: bzr merge lp:~percona-toolkit-dev/percona-toolkit/pt-agent
Reviewer Review Type Date Requested Status
Daniel Nichter Approve
Review via email: mp+169716@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Daniel Nichter (daniel-nichter) :
review: Approve
796. By Daniel Nichter

Fix typo in error message.

797. By Daniel Nichter

Add 2 safeguards to Agent::Logger to avoid excessive memory usage and error spamming.

798. By Daniel Nichter

Change the error spam threshold from 100 to 10.

799. By Daniel Nichter

Fix Logger error spamming.

800. By Daniel Nichter

Fix it again--the pitfalls of working so long and late.

801. By Daniel Nichter

Fix it again, again.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'bin/pt-agent'
2--- bin/pt-agent 1970-01-01 00:00:00 +0000
3+++ bin/pt-agent 2013-06-17 06:17:31 +0000
4@@ -0,0 +1,9343 @@
5+#!/usr/bin/env perl
6+
7+# This program is part of Percona Toolkit: http://www.percona.com/software/
8+# See "COPYRIGHT, LICENSE, AND WARRANTY" at the end of this file for legal
9+# notices and disclaimers.
10+
11+use strict;
12+use warnings FATAL => 'all';
13+
14+# This tool is "fat-packed": most of its dependent modules are embedded
15+# in this file. Setting %INC to this file for each module makes Perl aware
16+# of this so it will not try to load the module from @INC. See the tool's
17+# documentation for a full list of dependencies.
18+BEGIN {
19+ $INC{$_} = __FILE__ for map { (my $pkg = "$_.pm") =~ s!::!/!g; $pkg } (qw(
20+ Percona::Toolkit
21+ Lmo::Utils
22+ Lmo::Meta
23+ Lmo::Object
24+ Lmo::Types
25+ Lmo
26+ Percona::WebAPI::Representation
27+ Percona::WebAPI::Client
28+ Percona::WebAPI::Exception::Request
29+ Percona::WebAPI::Exception::Resource
30+ Percona::WebAPI::Resource::Agent
31+ Percona::WebAPI::Resource::Config
32+ Percona::WebAPI::Resource::Service
33+ Percona::WebAPI::Resource::Task
34+ Percona::WebAPI::Resource::LogEntry
35+ VersionCheck
36+ DSNParser
37+ OptionParser
38+ Cxn
39+ Quoter
40+ VersionParser
41+ Daemon
42+ Transformers
43+ Safeguards
44+ Percona::Agent::Logger
45+ ));
46+}
47+
48+# ###########################################################################
49+# Percona::Toolkit package
50+# This package is a copy without comments from the original. The original
51+# with comments and its test file can be found in the Bazaar repository at,
52+# lib/Percona/Toolkit.pm
53+# t/lib/Percona/Toolkit.t
54+# See https://launchpad.net/percona-toolkit for more information.
55+# ###########################################################################
56+{
57+package Percona::Toolkit;
58+
59+our $VERSION = '2.2.2';
60+
61+use strict;
62+use warnings FATAL => 'all';
63+use English qw(-no_match_vars);
64+use constant PTDEBUG => $ENV{PTDEBUG} || 0;
65+
66+use Carp qw(carp cluck);
67+use Data::Dumper qw();
68+
69+require Exporter;
70+our @ISA = qw(Exporter);
71+our @EXPORT_OK = qw(
72+ have_required_args
73+ Dumper
74+ _d
75+);
76+
77+sub have_required_args {
78+ my ($args, @required_args) = @_;
79+ my $have_required_args = 1;
80+ foreach my $arg ( @required_args ) {
81+ if ( !defined $args->{$arg} ) {
82+ $have_required_args = 0;
83+ carp "Argument $arg is not defined";
84+ }
85+ }
86+ cluck unless $have_required_args; # print backtrace
87+ return $have_required_args;
88+}
89+
90+sub Dumper {
91+ local $Data::Dumper::Indent = 1;
92+ local $Data::Dumper::Sortkeys = 1;
93+ local $Data::Dumper::Quotekeys = 0;
94+ Data::Dumper::Dumper(@_);
95+}
96+
97+sub _d {
98+ my ($package, undef, $line) = caller 0;
99+ @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
100+ map { defined $_ ? $_ : 'undef' }
101+ @_;
102+ print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
103+}
104+
105+1;
106+}
107+# ###########################################################################
108+# End Percona::Toolkit package
109+# ###########################################################################
110+
111+# ###########################################################################
112+# Lmo::Utils package
113+# This package is a copy without comments from the original. The original
114+# with comments and its test file can be found in the Bazaar repository at,
115+# lib/Lmo/Utils.pm
116+# t/lib/Lmo/Utils.t
117+# See https://launchpad.net/percona-toolkit for more information.
118+# ###########################################################################
119+{
120+package Lmo::Utils;
121+
122+use strict;
123+use warnings qw( FATAL all );
124+require Exporter;
125+our (@ISA, @EXPORT, @EXPORT_OK);
126+
127+BEGIN {
128+ @ISA = qw(Exporter);
129+ @EXPORT = @EXPORT_OK = qw(
130+ _install_coderef
131+ _unimport_coderefs
132+ _glob_for
133+ _stash_for
134+ );
135+}
136+
137+{
138+ no strict 'refs';
139+ sub _glob_for {
140+ return \*{shift()}
141+ }
142+
143+ sub _stash_for {
144+ return \%{ shift() . "::" };
145+ }
146+}
147+
148+sub _install_coderef {
149+ my ($to, $code) = @_;
150+
151+ return *{ _glob_for $to } = $code;
152+}
153+
154+sub _unimport_coderefs {
155+ my ($target, @names) = @_;
156+ return unless @names;
157+ my $stash = _stash_for($target);
158+ foreach my $name (@names) {
159+ if ($stash->{$name} and defined(&{$stash->{$name}})) {
160+ delete $stash->{$name};
161+ }
162+ }
163+}
164+
165+1;
166+}
167+# ###########################################################################
168+# End Lmo::Utils package
169+# ###########################################################################
170+
171+# ###########################################################################
172+# Lmo::Meta package
173+# This package is a copy without comments from the original. The original
174+# with comments and its test file can be found in the Bazaar repository at,
175+# lib/Lmo/Meta.pm
176+# t/lib/Lmo/Meta.t
177+# See https://launchpad.net/percona-toolkit for more information.
178+# ###########################################################################
179+{
180+package Lmo::Meta;
181+use strict;
182+use warnings qw( FATAL all );
183+
184+my %metadata_for;
185+
186+sub new {
187+ my $class = shift;
188+ return bless { @_ }, $class
189+}
190+
191+sub metadata_for {
192+ my $self = shift;
193+ my ($class) = @_;
194+
195+ return $metadata_for{$class} ||= {};
196+}
197+
198+sub class { shift->{class} }
199+
200+sub attributes {
201+ my $self = shift;
202+ return keys %{$self->metadata_for($self->class)}
203+}
204+
205+sub attributes_for_new {
206+ my $self = shift;
207+ my @attributes;
208+
209+ my $class_metadata = $self->metadata_for($self->class);
210+ while ( my ($attr, $meta) = each %$class_metadata ) {
211+ if ( exists $meta->{init_arg} ) {
212+ push @attributes, $meta->{init_arg}
213+ if defined $meta->{init_arg};
214+ }
215+ else {
216+ push @attributes, $attr;
217+ }
218+ }
219+ return @attributes;
220+}
221+
222+1;
223+}
224+# ###########################################################################
225+# End Lmo::Meta package
226+# ###########################################################################
227+
228+# ###########################################################################
229+# Lmo::Object package
230+# This package is a copy without comments from the original. The original
231+# with comments and its test file can be found in the Bazaar repository at,
232+# lib/Lmo/Object.pm
233+# t/lib/Lmo/Object.t
234+# See https://launchpad.net/percona-toolkit for more information.
235+# ###########################################################################
236+{
237+package Lmo::Object;
238+
239+use strict;
240+use warnings qw( FATAL all );
241+
242+use Carp ();
243+use Scalar::Util qw(blessed);
244+
245+use Lmo::Meta;
246+use Lmo::Utils qw(_glob_for);
247+
248+sub new {
249+ my $class = shift;
250+ my $args = $class->BUILDARGS(@_);
251+
252+ my $class_metadata = Lmo::Meta->metadata_for($class);
253+
254+ my @args_to_delete;
255+ while ( my ($attr, $meta) = each %$class_metadata ) {
256+ next unless exists $meta->{init_arg};
257+ my $init_arg = $meta->{init_arg};
258+
259+ if ( defined $init_arg ) {
260+ $args->{$attr} = delete $args->{$init_arg};
261+ }
262+ else {
263+ push @args_to_delete, $attr;
264+ }
265+ }
266+
267+ delete $args->{$_} for @args_to_delete;
268+
269+ for my $attribute ( keys %$args ) {
270+ if ( my $coerce = $class_metadata->{$attribute}{coerce} ) {
271+ $args->{$attribute} = $coerce->($args->{$attribute});
272+ }
273+ if ( my $isa_check = $class_metadata->{$attribute}{isa} ) {
274+ my ($check_name, $check_sub) = @$isa_check;
275+ $check_sub->($args->{$attribute});
276+ }
277+ }
278+
279+ while ( my ($attribute, $meta) = each %$class_metadata ) {
280+ next unless $meta->{required};
281+ Carp::confess("Attribute ($attribute) is required for $class")
282+ if ! exists $args->{$attribute}
283+ }
284+
285+ my $self = bless $args, $class;
286+
287+ my @build_subs;
288+ my $linearized_isa = mro::get_linear_isa($class);
289+
290+ for my $isa_class ( @$linearized_isa ) {
291+ unshift @build_subs, *{ _glob_for "${isa_class}::BUILD" }{CODE};
292+ }
293+ my @args = %$args;
294+ for my $sub (grep { defined($_) && exists &$_ } @build_subs) {
295+ $sub->( $self, @args);
296+ }
297+ return $self;
298+}
299+
300+sub BUILDARGS {
301+ shift; # No need for the classname
302+ if ( @_ == 1 && ref($_[0]) ) {
303+ Carp::confess("Single parameters to new() must be a HASH ref, not $_[0]")
304+ unless ref($_[0]) eq ref({});
305+ return {%{$_[0]}} # We want a new reference, always
306+ }
307+ else {
308+ return { @_ };
309+ }
310+}
311+
312+sub meta {
313+ my $class = shift;
314+ $class = Scalar::Util::blessed($class) || $class;
315+ return Lmo::Meta->new(class => $class);
316+}
317+
318+1;
319+}
320+# ###########################################################################
321+# End Lmo::Object package
322+# ###########################################################################
323+
324+# ###########################################################################
325+# Lmo::Types package
326+# This package is a copy without comments from the original. The original
327+# with comments and its test file can be found in the Bazaar repository at,
328+# lib/Lmo/Types.pm
329+# t/lib/Lmo/Types.t
330+# See https://launchpad.net/percona-toolkit for more information.
331+# ###########################################################################
332+{
333+package Lmo::Types;
334+
335+use strict;
336+use warnings qw( FATAL all );
337+
338+use Carp ();
339+use Scalar::Util qw(looks_like_number blessed);
340+
341+
342+our %TYPES = (
343+ Bool => sub { !$_[0] || (defined $_[0] && looks_like_number($_[0]) && $_[0] == 1) },
344+ Num => sub { defined $_[0] && looks_like_number($_[0]) },
345+ Int => sub { defined $_[0] && looks_like_number($_[0]) && $_[0] == int($_[0]) },
346+ Str => sub { defined $_[0] },
347+ Object => sub { defined $_[0] && blessed($_[0]) },
348+ FileHandle => sub { local $@; require IO::Handle; fileno($_[0]) && $_[0]->opened },
349+
350+ map {
351+ my $type = /R/ ? $_ : uc $_;
352+ $_ . "Ref" => sub { ref $_[0] eq $type }
353+ } qw(Array Code Hash Regexp Glob Scalar)
354+);
355+
356+sub check_type_constaints {
357+ my ($attribute, $type_check, $check_name, $val) = @_;
358+ ( ref($type_check) eq 'CODE'
359+ ? $type_check->($val)
360+ : (ref $val eq $type_check
361+ || ($val && $val eq $type_check)
362+ || (exists $TYPES{$type_check} && $TYPES{$type_check}->($val)))
363+ )
364+ || Carp::confess(
365+ qq<Attribute ($attribute) does not pass the type constraint because: >
366+ . qq<Validation failed for '$check_name' with value >
367+ . (defined $val ? Lmo::Dumper($val) : 'undef') )
368+}
369+
370+sub _nested_constraints {
371+ my ($attribute, $aggregate_type, $type) = @_;
372+
373+ my $inner_types;
374+ if ( $type =~ /\A(ArrayRef|Maybe)\[(.*)\]\z/ ) {
375+ $inner_types = _nested_constraints($1, $2);
376+ }
377+ else {
378+ $inner_types = $TYPES{$type};
379+ }
380+
381+ if ( $aggregate_type eq 'ArrayRef' ) {
382+ return sub {
383+ my ($val) = @_;
384+ return unless ref($val) eq ref([]);
385+
386+ if ($inner_types) {
387+ for my $value ( @{$val} ) {
388+ return unless $inner_types->($value)
389+ }
390+ }
391+ else {
392+ for my $value ( @{$val} ) {
393+ return unless $value && ($value eq $type
394+ || (Scalar::Util::blessed($value) && $value->isa($type)));
395+ }
396+ }
397+ return 1;
398+ };
399+ }
400+ elsif ( $aggregate_type eq 'Maybe' ) {
401+ return sub {
402+ my ($value) = @_;
403+ return 1 if ! defined($value);
404+ if ($inner_types) {
405+ return unless $inner_types->($value)
406+ }
407+ else {
408+ return unless $value eq $type
409+ || (Scalar::Util::blessed($value) && $value->isa($type));
410+ }
411+ return 1;
412+ }
413+ }
414+ else {
415+ Carp::confess("Nested aggregate types are only implemented for ArrayRefs and Maybe");
416+ }
417+}
418+
419+1;
420+}
421+# ###########################################################################
422+# End Lmo::Types package
423+# ###########################################################################
424+
425+# ###########################################################################
426+# Lmo package
427+# This package is a copy without comments from the original. The original
428+# with comments and its test file can be found in the Bazaar repository at,
429+# lib/Lmo.pm
430+# t/lib/Lmo.t
431+# See https://launchpad.net/percona-toolkit for more information.
432+# ###########################################################################
433+{
434+BEGIN {
435+$INC{"Lmo.pm"} = __FILE__;
436+package Lmo;
437+our $VERSION = '0.30_Percona'; # Forked from 0.30 of Mo.
438+
439+
440+use strict;
441+use warnings qw( FATAL all );
442+
443+use Carp ();
444+use Scalar::Util qw(looks_like_number blessed);
445+
446+use Lmo::Meta;
447+use Lmo::Object;
448+use Lmo::Types;
449+
450+use Lmo::Utils;
451+
452+my %export_for;
453+sub import {
454+ warnings->import(qw(FATAL all));
455+ strict->import();
456+
457+ my $caller = scalar caller(); # Caller's package
458+ my %exports = (
459+ extends => \&extends,
460+ has => \&has,
461+ with => \&with,
462+ override => \&override,
463+ confess => \&Carp::confess,
464+ );
465+
466+ $export_for{$caller} = \%exports;
467+
468+ for my $keyword ( keys %exports ) {
469+ _install_coderef "${caller}::$keyword" => $exports{$keyword};
470+ }
471+
472+ if ( !@{ *{ _glob_for "${caller}::ISA" }{ARRAY} || [] } ) {
473+ @_ = "Lmo::Object";
474+ goto *{ _glob_for "${caller}::extends" }{CODE};
475+ }
476+}
477+
478+sub extends {
479+ my $caller = scalar caller();
480+ for my $class ( @_ ) {
481+ _load_module($class);
482+ }
483+ _set_package_isa($caller, @_);
484+ _set_inherited_metadata($caller);
485+}
486+
487+sub _load_module {
488+ my ($class) = @_;
489+
490+ (my $file = $class) =~ s{::|'}{/}g;
491+ $file .= '.pm';
492+ { local $@; eval { require "$file" } } # or warn $@;
493+ return;
494+}
495+
496+sub with {
497+ my $package = scalar caller();
498+ require Role::Tiny;
499+ for my $role ( @_ ) {
500+ _load_module($role);
501+ _role_attribute_metadata($package, $role);
502+ }
503+ Role::Tiny->apply_roles_to_package($package, @_);
504+}
505+
506+sub _role_attribute_metadata {
507+ my ($package, $role) = @_;
508+
509+ my $package_meta = Lmo::Meta->metadata_for($package);
510+ my $role_meta = Lmo::Meta->metadata_for($role);
511+
512+ %$package_meta = (%$role_meta, %$package_meta);
513+}
514+
515+sub has {
516+ my $names = shift;
517+ my $caller = scalar caller();
518+
519+ my $class_metadata = Lmo::Meta->metadata_for($caller);
520+
521+ for my $attribute ( ref $names ? @$names : $names ) {
522+ my %args = @_;
523+ my $method = ($args{is} || '') eq 'ro'
524+ ? sub {
525+ Carp::confess("Cannot assign a value to a read-only accessor at reader ${caller}::${attribute}")
526+ if $#_;
527+ return $_[0]{$attribute};
528+ }
529+ : sub {
530+ return $#_
531+ ? $_[0]{$attribute} = $_[1]
532+ : $_[0]{$attribute};
533+ };
534+
535+ $class_metadata->{$attribute} = ();
536+
537+ if ( my $type_check = $args{isa} ) {
538+ my $check_name = $type_check;
539+
540+ if ( my ($aggregate_type, $inner_type) = $type_check =~ /\A(ArrayRef|Maybe)\[(.*)\]\z/ ) {
541+ $type_check = Lmo::Types::_nested_constraints($attribute, $aggregate_type, $inner_type);
542+ }
543+
544+ my $check_sub = sub {
545+ my ($new_val) = @_;
546+ Lmo::Types::check_type_constaints($attribute, $type_check, $check_name, $new_val);
547+ };
548+
549+ $class_metadata->{$attribute}{isa} = [$check_name, $check_sub];
550+ my $orig_method = $method;
551+ $method = sub {
552+ $check_sub->($_[1]) if $#_;
553+ goto &$orig_method;
554+ };
555+ }
556+
557+ if ( my $builder = $args{builder} ) {
558+ my $original_method = $method;
559+ $method = sub {
560+ $#_
561+ ? goto &$original_method
562+ : ! exists $_[0]{$attribute}
563+ ? $_[0]{$attribute} = $_[0]->$builder
564+ : goto &$original_method
565+ };
566+ }
567+
568+ if ( my $code = $args{default} ) {
569+ Carp::confess("${caller}::${attribute}'s default is $code, but should be a coderef")
570+ unless ref($code) eq 'CODE';
571+ my $original_method = $method;
572+ $method = sub {
573+ $#_
574+ ? goto &$original_method
575+ : ! exists $_[0]{$attribute}
576+ ? $_[0]{$attribute} = $_[0]->$code
577+ : goto &$original_method
578+ };
579+ }
580+
581+ if ( my $role = $args{does} ) {
582+ my $original_method = $method;
583+ $method = sub {
584+ if ( $#_ ) {
585+ Carp::confess(qq<Attribute ($attribute) doesn't consume a '$role' role">)
586+ unless Scalar::Util::blessed($_[1]) && eval { $_[1]->does($role) }
587+ }
588+ goto &$original_method
589+ };
590+ }
591+
592+ if ( my $coercion = $args{coerce} ) {
593+ $class_metadata->{$attribute}{coerce} = $coercion;
594+ my $original_method = $method;
595+ $method = sub {
596+ if ( $#_ ) {
597+ return $original_method->($_[0], $coercion->($_[1]))
598+ }
599+ goto &$original_method;
600+ }
601+ }
602+
603+ _install_coderef "${caller}::$attribute" => $method;
604+
605+ if ( $args{required} ) {
606+ $class_metadata->{$attribute}{required} = 1;
607+ }
608+
609+ if ($args{clearer}) {
610+ _install_coderef "${caller}::$args{clearer}"
611+ => sub { delete shift->{$attribute} }
612+ }
613+
614+ if ($args{predicate}) {
615+ _install_coderef "${caller}::$args{predicate}"
616+ => sub { exists shift->{$attribute} }
617+ }
618+
619+ if ($args{handles}) {
620+ _has_handles($caller, $attribute, \%args);
621+ }
622+
623+ if (exists $args{init_arg}) {
624+ $class_metadata->{$attribute}{init_arg} = $args{init_arg};
625+ }
626+ }
627+}
628+
629+sub _has_handles {
630+ my ($caller, $attribute, $args) = @_;
631+ my $handles = $args->{handles};
632+
633+ my $ref = ref $handles;
634+ my $kv;
635+ if ( $ref eq ref [] ) {
636+ $kv = { map { $_,$_ } @{$handles} };
637+ }
638+ elsif ( $ref eq ref {} ) {
639+ $kv = $handles;
640+ }
641+ elsif ( $ref eq ref qr// ) {
642+ Carp::confess("Cannot delegate methods based on a Regexp without a type constraint (isa)")
643+ unless $args->{isa};
644+ my $target_class = $args->{isa};
645+ $kv = {
646+ map { $_, $_ }
647+ grep { $_ =~ $handles }
648+ grep { !exists $Lmo::Object::{$_} && $target_class->can($_) }
649+ grep { !$export_for{$target_class}->{$_} }
650+ keys %{ _stash_for $target_class }
651+ };
652+ }
653+ else {
654+ Carp::confess("handles for $ref not yet implemented");
655+ }
656+
657+ while ( my ($method, $target) = each %{$kv} ) {
658+ my $name = _glob_for "${caller}::$method";
659+ Carp::confess("You cannot overwrite a locally defined method ($method) with a delegation")
660+ if defined &$name;
661+
662+ my ($target, @curried_args) = ref($target) ? @$target : $target;
663+ *$name = sub {
664+ my $self = shift;
665+ my $delegate_to = $self->$attribute();
666+ my $error = "Cannot delegate $method to $target because the value of $attribute";
667+ Carp::confess("$error is not defined") unless $delegate_to;
668+ Carp::confess("$error is not an object (got '$delegate_to')")
669+ unless Scalar::Util::blessed($delegate_to) || (!ref($delegate_to) && $delegate_to->can($target));
670+ return $delegate_to->$target(@curried_args, @_);
671+ }
672+ }
673+}
674+
675+sub _set_package_isa {
676+ my ($package, @new_isa) = @_;
677+ my $package_isa = \*{ _glob_for "${package}::ISA" };
678+ @{*$package_isa} = @new_isa;
679+}
680+
681+sub _set_inherited_metadata {
682+ my $class = shift;
683+ my $class_metadata = Lmo::Meta->metadata_for($class);
684+ my $linearized_isa = mro::get_linear_isa($class);
685+ my %new_metadata;
686+
687+ for my $isa_class (reverse @$linearized_isa) {
688+ my $isa_metadata = Lmo::Meta->metadata_for($isa_class);
689+ %new_metadata = (
690+ %new_metadata,
691+ %$isa_metadata,
692+ );
693+ }
694+ %$class_metadata = %new_metadata;
695+}
696+
697+sub unimport {
698+ my $caller = scalar caller();
699+ my $target = caller;
700+ _unimport_coderefs($target, keys %{$export_for{$caller}});
701+}
702+
703+sub Dumper {
704+ require Data::Dumper;
705+ local $Data::Dumper::Indent = 0;
706+ local $Data::Dumper::Sortkeys = 0;
707+ local $Data::Dumper::Quotekeys = 0;
708+ local $Data::Dumper::Terse = 1;
709+
710+ Data::Dumper::Dumper(@_)
711+}
712+
713+BEGIN {
714+ if ($] >= 5.010) {
715+ { local $@; require mro; }
716+ }
717+ else {
718+ local $@;
719+ eval {
720+ require MRO::Compat;
721+ } or do {
722+ *mro::get_linear_isa = *mro::get_linear_isa_dfs = sub {
723+ no strict 'refs';
724+
725+ my $classname = shift;
726+
727+ my @lin = ($classname);
728+ my %stored;
729+ foreach my $parent (@{"$classname\::ISA"}) {
730+ my $plin = mro::get_linear_isa_dfs($parent);
731+ foreach (@$plin) {
732+ next if exists $stored{$_};
733+ push(@lin, $_);
734+ $stored{$_} = 1;
735+ }
736+ }
737+ return \@lin;
738+ };
739+ }
740+ }
741+}
742+
743+sub override {
744+ my ($methods, $code) = @_;
745+ my $caller = scalar caller;
746+
747+ for my $method ( ref($methods) ? @$methods : $methods ) {
748+ my $full_method = "${caller}::${method}";
749+ *{_glob_for $full_method} = $code;
750+ }
751+}
752+
753+}
754+1;
755+}
756+# ###########################################################################
757+# End Lmo package
758+# ###########################################################################
759+
760+# ###########################################################################
761+# Percona::WebAPI::Representation package
762+# This package is a copy without comments from the original. The original
763+# with comments and its test file can be found in the Bazaar repository at,
764+# lib/Percona/WebAPI/Representation.pm
765+# t/lib/Percona/WebAPI/Representation.t
766+# See https://launchpad.net/percona-toolkit for more information.
767+# ###########################################################################
768+{
769+package Percona::WebAPI::Representation;
770+
771+eval {
772+ require JSON;
773+};
774+
775+require Exporter;
776+our @ISA = qw(Exporter);
777+our @EXPORT_OK = qw(
778+ as_hashref
779+ as_json
780+ as_config
781+);
782+
783+sub as_hashref {
784+ my ($resource, %args) = @_;
785+
786+ my $as_hashref = { %$resource };
787+
788+ if ( !defined $args{with_links} || !$args{with_links} ) {
789+ delete $as_hashref->{links};
790+ }
791+
792+ return $as_hashref;
793+}
794+
795+sub as_json {
796+ my ($resource, %args) = @_;
797+
798+ my $json = $args{json} || JSON->new;
799+ $json->allow_blessed([]);
800+ $json->convert_blessed([]);
801+
802+ my $text = $json->encode(
803+ ref $resource eq 'ARRAY' ? $resource : as_hashref($resource, %args)
804+ );
805+ if ( $args{json} && $text ) { # for testing
806+ chomp($text);
807+ $text .= "\n";
808+ }
809+ return $text;
810+}
811+
812+sub as_config {
813+ my $resource = shift;
814+ if ( !$resource->isa('Percona::WebAPI::Resource::Config') ) {
815+ die "Only Config resources can be represented as config.\n";
816+ }
817+ my $as_hashref = as_hashref($resource);
818+ my $options = $as_hashref->{options};
819+ my $config = join("\n",
820+ map { defined $options->{$_} ? "$_=$options->{$_}" : "$_" }
821+ sort keys %$options
822+ ) . "\n";
823+ return $config;
824+}
825+
826+1;
827+}
828+# ###########################################################################
829+# End Percona::WebAPI::Representation package
830+# ###########################################################################
831+
832+# ###########################################################################
833+# Percona::WebAPI::Client package
834+# This package is a copy without comments from the original. The original
835+# with comments and its test file can be found in the Bazaar repository at,
836+# lib/Percona/WebAPI/Client.pm
837+# t/lib/Percona/WebAPI/Client.t
838+# See https://launchpad.net/percona-toolkit for more information.
839+# ###########################################################################
840+{
841+package Percona::WebAPI::Client;
842+
843+our $VERSION = '0.01';
844+
845+use strict;
846+use warnings FATAL => 'all';
847+use English qw(-no_match_vars);
848+use constant PTDEBUG => $ENV{PTDEBUG} || 0;
849+
850+eval {
851+ require LWP;
852+ require JSON;
853+};
854+
855+use Scalar::Util qw(blessed);
856+
857+use Lmo;
858+use Percona::Toolkit;
859+use Percona::WebAPI::Representation;
860+use Percona::WebAPI::Exception::Request;
861+use Percona::WebAPI::Exception::Resource;
862+
863+Percona::WebAPI::Representation->import(qw(as_json));
864+Percona::Toolkit->import(qw(_d Dumper have_required_args));
865+
866+has 'api_key' => (
867+ is => 'ro',
868+ isa => 'Str',
869+ required => 1,
870+);
871+
872+has 'entry_link' => (
873+ is => 'rw',
874+ isa => 'Str',
875+ required => 0,
876+ default => sub { return 'https://cloud-api.percona.com' },
877+);
878+
879+has 'ua' => (
880+ is => 'rw',
881+ isa => 'Object',
882+ lazy => 1,
883+ required => 0,
884+ builder => '_build_ua',
885+);
886+
887+has 'response' => (
888+ is => 'rw',
889+ isa => 'Object',
890+ required => 0,
891+ default => undef,
892+);
893+
894+sub _build_ua {
895+ my $self = shift;
896+ my $ua = LWP::UserAgent->new;
897+ $ua->agent("Percona::WebAPI::Client/$Percona::WebAPI::Client::VERSION");
898+ $ua->default_header('Content-Type', 'application/json');
899+ $ua->default_header('X-Percona-API-Key', $self->api_key);
900+ return $ua;
901+}
902+
903+sub get {
904+ my ($self, %args) = @_;
905+
906+ have_required_args(\%args, qw(
907+ link
908+ )) or die;
909+ my ($link) = $args{link};
910+
911+ eval {
912+ $self->_request(
913+ method => 'GET',
914+ link => $link,
915+ );
916+ };
917+ if ( my $e = $EVAL_ERROR ) {
918+ if (blessed($e) && $e->isa('Percona::WebAPI::Exception::Request')) {
919+ die $e;
920+ }
921+ else {
922+ die "Unknown error: $e";
923+ }
924+ }
925+
926+ my $resource = eval {
927+ JSON::decode_json($self->response->content);
928+ };
929+ if ( $EVAL_ERROR ) {
930+ warn sprintf "Error decoding resource: %s: %s",
931+ $self->response->content,
932+ $EVAL_ERROR;
933+ return;
934+ }
935+
936+ my $resource_objects;
937+ if ( my $type = $self->response->headers->{'x-percona-resource-type'} ) {
938+ eval {
939+ $type = "Percona::WebAPI::Resource::$type";
940+ if ( ref $resource eq 'ARRAY' ) {
941+ PTDEBUG && _d('Got a list of', $type, 'resources');
942+ $resource_objects = [];
943+ foreach my $attribs ( @$resource ) {
944+ my $obj = $type->new(%$attribs);
945+ push @$resource_objects, $obj;
946+ }
947+ }
948+ else {
949+ PTDEBUG && _d('Got a', $type, 'resource', Dumper($resource));
950+ $resource_objects = $type->new(%$resource);
951+ }
952+ };
953+ if ( my $e = $EVAL_ERROR ) {
954+ die Percona::WebAPI::Exception::Resource->new(
955+ type => $type,
956+ link => $link,
957+ data => (ref $resource eq 'ARRAY' ? $resource : [ $resource ]),
958+ error => $e,
959+ );
960+ }
961+ }
962+ elsif ( exists $resource->{links} ) {
963+ $resource_objects = $resource->{links};
964+ }
965+ else {
966+ warn "Did not get X-Percona-Resource-Type or links from $link\n";
967+ }
968+
969+ return $resource_objects;
970+}
971+
972+sub post {
973+ my $self = shift;
974+ $self->_set(
975+ @_,
976+ method => 'POST',
977+ );
978+ return $self->response->header('Location');
979+}
980+
981+sub put {
982+ my $self = shift;
983+ $self->_set(
984+ @_,
985+ method => 'PUT',
986+ );
987+ return $self->response->header('Location');
988+}
989+
990+sub delete {
991+ my ($self, %args) = @_;
992+ have_required_args(\%args, qw(
993+ link
994+ )) or die;
995+ my ($link) = $args{link};
996+
997+ eval {
998+ $self->_request(
999+ method => 'DELETE',
1000+ link => $link,
1001+ headers => { 'Content-Length' => 0 },
1002+ );
1003+ };
1004+ if ( my $e = $EVAL_ERROR ) {
1005+ if (blessed($e) && $e->isa('Percona::WebAPI::Exception::Request')) {
1006+ die $e;
1007+ }
1008+ else {
1009+ die "Unknown error: $e";
1010+ }
1011+ }
1012+
1013+ return;
1014+}
1015+
1016+sub _set {
1017+ my ($self, %args) = @_;
1018+ have_required_args(\%args, qw(
1019+ method
1020+ resources
1021+ link
1022+ )) or die;
1023+ my $method = $args{method};
1024+ my $res = $args{resources};
1025+ my $link = $args{link};
1026+
1027+ my $headers = $args{headers};
1028+
1029+ my $content = '';
1030+ if ( ref($res) eq 'ARRAY' ) {
1031+ PTDEBUG && _d('List of resources');
1032+ $content = '[' . join(",\n", map { as_json($_) } @$res) . ']';
1033+ }
1034+ elsif ( ref($res) ) {
1035+ PTDEBUG && _d('Resource object');
1036+ $content = as_json($res);
1037+ }
1038+ elsif ( $res !~ m/\n/ && -f $res ) {
1039+ PTDEBUG && _d('List of resources in file', $res);
1040+ $content = '[';
1041+ my $data = do {
1042+ local $INPUT_RECORD_SEPARATOR = undef;
1043+ open my $fh, '<', $res
1044+ or die "Error opening $res: $OS_ERROR";
1045+ <$fh>;
1046+ };
1047+ $data =~ s/,?\s*$/]/;
1048+ $content .= $data;
1049+ }
1050+ else {
1051+ PTDEBUG && _d('Resource text');
1052+ $content = $res;
1053+ }
1054+
1055+ eval {
1056+ $self->_request(
1057+ method => $method,
1058+ link => $link,
1059+ content => $content,
1060+ headers => $headers,
1061+ );
1062+ };
1063+ if ( my $e = $EVAL_ERROR ) {
1064+ if (blessed($e) && $e->isa('Percona::WebAPI::Exception::Request')) {
1065+ die $e;
1066+ }
1067+ else {
1068+ die "Unknown error: $e";
1069+ }
1070+ }
1071+
1072+ return;
1073+}
1074+
1075+sub _request {
1076+ my ($self, %args) = @_;
1077+
1078+ have_required_args(\%args, qw(
1079+ method
1080+ link
1081+ )) or die;
1082+ my $method = $args{method};
1083+ my $link = $args{link};
1084+
1085+ my $content = $args{content};
1086+ my $headers = $args{headers};
1087+
1088+ my $req = HTTP::Request->new($method => $link);
1089+ if ( $content ) {
1090+ $req->content($content);
1091+ }
1092+ if ( $headers ) {
1093+ map { $req->header($_ => $headers->{$_}) } keys %$headers;
1094+ }
1095+ PTDEBUG && _d('Request', $method, $link, Dumper($req));
1096+
1097+ my $response = $self->ua->request($req);
1098+ PTDEBUG && _d('Response', Dumper($response));
1099+
1100+ $self->response($response);
1101+
1102+ if ( !($response->code >= 200 && $response->code < 400) ) {
1103+ die Percona::WebAPI::Exception::Request->new(
1104+ method => $method,
1105+ url => $link,
1106+ content => $content,
1107+ status => $response->code,
1108+ error => "Failed to $method $link",
1109+ );
1110+ }
1111+
1112+ return;
1113+}
1114+
1115+no Lmo;
1116+1;
1117+}
1118+# ###########################################################################
1119+# End Percona::WebAPI::Client package
1120+# ###########################################################################
1121+
1122+# ###########################################################################
1123+# Percona::WebAPI::Exception::Request package
1124+# This package is a copy without comments from the original. The original
1125+# with comments and its test file can be found in the Bazaar repository at,
1126+# lib/Percona/WebAPI/Exception/Request.pm
1127+# t/lib/Percona/WebAPI/Exception/Request.t
1128+# See https://launchpad.net/percona-toolkit for more information.
1129+# ###########################################################################
1130+{
1131+package Percona::WebAPI::Exception::Request;
1132+
1133+use Lmo;
1134+use overload '""' => \&as_string;
1135+
1136+has 'method' => (
1137+ is => 'ro',
1138+ isa => 'Str',
1139+ required => 1,
1140+);
1141+
1142+has 'url' => (
1143+ is => 'ro',
1144+ isa => 'Str',
1145+ required => 1,
1146+);
1147+
1148+has 'content' => (
1149+ is => 'ro',
1150+ isa => 'Maybe[Str]',
1151+ required => 0,
1152+);
1153+
1154+has 'status' => (
1155+ is => 'ro',
1156+ isa => 'Int',
1157+ required => 1,
1158+);
1159+
1160+has 'error' => (
1161+ is => 'ro',
1162+ isa => 'Str',
1163+ required => 1,
1164+);
1165+
1166+sub as_string {
1167+ my $self = shift;
1168+ chomp(my $error = $self->error);
1169+ $error =~ s/\n/ /g;
1170+ return sprintf "%s\nRequest: %s %s %s\nStatus: %d\n",
1171+ $error, $self->method, $self->url, $self->content || '', $self->status;
1172+}
1173+
1174+no Lmo;
1175+1;
1176+}
1177+# ###########################################################################
1178+# End Percona::WebAPI::Exception::Request package
1179+# ###########################################################################
1180+
1181+# ###########################################################################
1182+# Percona::WebAPI::Exception::Resource package
1183+# This package is a copy without comments from the original. The original
1184+# with comments and its test file can be found in the Bazaar repository at,
1185+# lib/Percona/WebAPI/Exception/Resource.pm
1186+# t/lib/Percona/WebAPI/Exception/Resource.t
1187+# See https://launchpad.net/percona-toolkit for more information.
1188+# ###########################################################################
1189+{
1190+package Percona::WebAPI::Exception::Resource;
1191+
1192+use Lmo;
1193+use overload '""' => \&as_string;
1194+use Data::Dumper;
1195+
1196+has 'type' => (
1197+ is => 'ro',
1198+ isa => 'Str',
1199+ required => 1,
1200+);
1201+
1202+has 'link' => (
1203+ is => 'ro',
1204+ isa => 'Str',
1205+ required => 1,
1206+);
1207+
1208+has 'data' => (
1209+ is => 'ro',
1210+ isa => 'ArrayRef',
1211+ required => 1,
1212+);
1213+
1214+has 'error' => (
1215+ is => 'ro',
1216+ isa => 'Str',
1217+ required => 1,
1218+);
1219+
1220+sub as_string {
1221+ my $self = shift;
1222+ chomp(my $error = $self->error);
1223+ local $Data::Dumper::Indent = 1;
1224+ local $Data::Dumper::Sortkeys = 1;
1225+ local $Data::Dumper::Quotekeys = 0;
1226+ return sprintf "Invalid %s resource from %s:\n\n%s\nError: %s\n\n",
1227+ $self->type, $self->link, Dumper($self->data), $error;
1228+}
1229+
1230+no Lmo;
1231+1;
1232+}
1233+# ###########################################################################
1234+# End Percona::WebAPI::Exception::Resource package
1235+# ###########################################################################
1236+
1237+# ###########################################################################
1238+# Percona::WebAPI::Resource::Agent package
1239+# This package is a copy without comments from the original. The original
1240+# with comments and its test file can be found in the Bazaar repository at,
1241+# lib/Percona/WebAPI/Resource/Agent.pm
1242+# t/lib/Percona/WebAPI/Resource/Agent.t
1243+# See https://launchpad.net/percona-toolkit for more information.
1244+# ###########################################################################
1245+{
1246+package Percona::WebAPI::Resource::Agent;
1247+
1248+use Lmo;
1249+
1250+has 'uuid' => (
1251+ is => 'ro',
1252+ isa => 'Str',
1253+ required => 0,
1254+);
1255+
1256+has 'username' => (
1257+ is => 'rw',
1258+ isa => 'Str',
1259+ required => 0,
1260+ default => sub { return $ENV{USER} || $ENV{LOGNAME} },
1261+);
1262+
1263+has 'hostname' => (
1264+ is => 'rw',
1265+ isa => 'Str',
1266+ required => 0,
1267+ default => sub {
1268+ chomp(my $hostname = `hostname`);
1269+ return $hostname;
1270+ },
1271+);
1272+
1273+has 'alias' => (
1274+ is => 'rw',
1275+ isa => 'Str',
1276+ required => 0,
1277+);
1278+
1279+has 'versions' => (
1280+ is => 'rw',
1281+ isa => 'Maybe[HashRef]',
1282+ required => 0,
1283+);
1284+
1285+has 'links' => (
1286+ is => 'rw',
1287+ isa => 'Maybe[HashRef]',
1288+ required => 0,
1289+ default => sub { return {} },
1290+);
1291+
1292+sub name {
1293+ my ($self) = @_;
1294+ return $self->alias || $self->hostname || $self->uuid || 'Unknown';
1295+}
1296+
1297+no Lmo;
1298+1;
1299+}
1300+# ###########################################################################
1301+# End Percona::WebAPI::Resource::Agent package
1302+# ###########################################################################
1303+
1304+# ###########################################################################
1305+# Percona::WebAPI::Resource::Config package
1306+# This package is a copy without comments from the original. The original
1307+# with comments and its test file can be found in the Bazaar repository at,
1308+# lib/Percona/WebAPI/Resource/Config.pm
1309+# t/lib/Percona/WebAPI/Resource/Config.t
1310+# See https://launchpad.net/percona-toolkit for more information.
1311+# ###########################################################################
1312+{
1313+package Percona::WebAPI::Resource::Config;
1314+
1315+use Lmo;
1316+
1317+has 'ts' => (
1318+ is => 'ro',
1319+ isa => 'Int',
1320+ required => 1,
1321+);
1322+
1323+has 'name' => (
1324+ is => 'ro',
1325+ isa => 'Str',
1326+ required => 1,
1327+);
1328+
1329+has 'options' => (
1330+ is => 'ro',
1331+ isa => 'HashRef',
1332+ required => 1,
1333+);
1334+
1335+has 'links' => (
1336+ is => 'rw',
1337+ isa => 'Maybe[HashRef]',
1338+ required => 0,
1339+ default => sub { return {} },
1340+);
1341+
1342+no Lmo;
1343+1;
1344+}
1345+# ###########################################################################
1346+# End Percona::WebAPI::Resource::Config package
1347+# ###########################################################################
1348+
1349+# ###########################################################################
1350+# Percona::WebAPI::Resource::Service package
1351+# This package is a copy without comments from the original. The original
1352+# with comments and its test file can be found in the Bazaar repository at,
1353+# lib/Percona/WebAPI/Resource/Service.pm
1354+# t/lib/Percona/WebAPI/Resource/Service.t
1355+# See https://launchpad.net/percona-toolkit for more information.
1356+# ###########################################################################
1357+{
1358+package Percona::WebAPI::Resource::Service;
1359+
1360+use Lmo;
1361+
1362+has 'ts' => (
1363+ is => 'ro',
1364+ isa => 'Int',
1365+ required => 1,
1366+);
1367+
1368+has 'name' => (
1369+ is => 'ro',
1370+ isa => 'Str',
1371+ required => 1,
1372+);
1373+
1374+has 'tasks' => (
1375+ is => 'ro',
1376+ isa => 'ArrayRef[Percona::WebAPI::Resource::Task]',
1377+ required => 1,
1378+);
1379+
1380+has 'run_schedule' => (
1381+ is => 'ro',
1382+ isa => 'Str',
1383+ required => 0,
1384+);
1385+
1386+has 'spool_schedule' => (
1387+ is => 'ro',
1388+ isa => 'Str',
1389+ required => 0,
1390+);
1391+
1392+has 'meta' => (
1393+ is => 'ro',
1394+ isa => 'Bool',
1395+ required => 0,
1396+ default => sub { return 0 },
1397+);
1398+
1399+has 'run_once' => (
1400+ is => 'ro',
1401+ isa => 'Bool',
1402+ required => 0,
1403+ default => sub { return 0 },
1404+);
1405+
1406+has 'links' => (
1407+ is => 'rw',
1408+ isa => 'Maybe[HashRef]',
1409+ required => 0,
1410+ default => sub { return {} },
1411+);
1412+
1413+sub BUILDARGS {
1414+ my ($class, %args) = @_;
1415+ if ( ref $args{tasks} eq 'ARRAY' ) {
1416+ my @tasks;
1417+ foreach my $run_hashref ( @{$args{tasks}} ) {
1418+ my $task = Percona::WebAPI::Resource::Task->new(%$run_hashref);
1419+ push @tasks, $task;
1420+ }
1421+ $args{tasks} = \@tasks;
1422+ }
1423+ return $class->SUPER::BUILDARGS(%args);
1424+}
1425+
1426+no Lmo;
1427+1;
1428+}
1429+# ###########################################################################
1430+# End Percona::WebAPI::Resource::Service package
1431+# ###########################################################################
1432+
1433+# ###########################################################################
1434+# Percona::WebAPI::Resource::Task package
1435+# This package is a copy without comments from the original. The original
1436+# with comments and its test file can be found in the Bazaar repository at,
1437+# lib/Percona/WebAPI/Resource/Task.pm
1438+# t/lib/Percona/WebAPI/Resource/Task.t
1439+# See https://launchpad.net/percona-toolkit for more information.
1440+# ###########################################################################
1441+{
1442+package Percona::WebAPI::Resource::Task;
1443+
1444+use Lmo;
1445+
1446+has 'name' => (
1447+ is => 'ro',
1448+ isa => 'Str',
1449+ required => 1,
1450+);
1451+
1452+has 'number' => (
1453+ is => 'ro',
1454+ isa => 'Int',
1455+ required => 1,
1456+);
1457+
1458+has 'program' => (
1459+ is => 'ro',
1460+ isa => 'Maybe[Str]',
1461+ required => 0,
1462+);
1463+
1464+has 'query' => (
1465+ is => 'ro',
1466+ isa => 'Maybe[Str]',
1467+ required => 0,
1468+);
1469+
1470+has 'output' => (
1471+ is => 'ro',
1472+ isa => 'Maybe[Str]',
1473+ required => 0,
1474+);
1475+
1476+sub TO_JSON { return { %{ shift() } }; }
1477+
1478+no Lmo;
1479+1;
1480+}
1481+# ###########################################################################
1482+# End Percona::WebAPI::Resource::Task package
1483+# ###########################################################################
1484+
1485+# ###########################################################################
1486+# Percona::WebAPI::Resource::LogEntry package
1487+# This package is a copy without comments from the original. The original
1488+# with comments and its test file can be found in the Bazaar repository at,
1489+# lib/Percona/WebAPI/Resource/LogEntry.pm
1490+# t/lib/Percona/WebAPI/Resource/LogEntry.t
1491+# See https://launchpad.net/percona-toolkit for more information.
1492+# ###########################################################################
1493+{
1494+package Percona::WebAPI::Resource::LogEntry;
1495+
1496+use Lmo;
1497+
1498+has 'pid' => (
1499+ is => 'ro',
1500+ isa => 'Int',
1501+ required => 1,
1502+);
1503+
1504+has 'service' => (
1505+ is => 'ro',
1506+ isa => 'Str',
1507+ required => 0,
1508+);
1509+
1510+has 'data_ts' => (
1511+ is => 'ro',
1512+ isa => 'Int',
1513+ required => 0,
1514+);
1515+
1516+has 'entry_ts' => (
1517+ is => 'ro',
1518+ isa => 'Str',
1519+ required => 1,
1520+);
1521+
1522+has 'log_level' => (
1523+ is => 'ro',
1524+ isa => 'Int',
1525+ required => 1,
1526+);
1527+
1528+has 'message' => (
1529+ is => 'ro',
1530+ isa => 'Str',
1531+ required => 1,
1532+);
1533+
1534+no Lmo;
1535+1;
1536+}
1537+# ###########################################################################
1538+# End Percona::WebAPI::Resource::LogEntry package
1539+# ###########################################################################
1540+
1541+# ###########################################################################
1542+# VersionCheck package
1543+# This package is a copy without comments from the original. The original
1544+# with comments and its test file can be found in the Bazaar repository at,
1545+# lib/VersionCheck.pm
1546+# t/lib/VersionCheck.t
1547+# See https://launchpad.net/percona-toolkit for more information.
1548+# ###########################################################################
1549+{
1550+package VersionCheck;
1551+
1552+
1553+use strict;
1554+use warnings FATAL => 'all';
1555+use English qw(-no_match_vars);
1556+
1557+use constant PTDEBUG => $ENV{PTDEBUG} || 0;
1558+
1559+use Data::Dumper;
1560+local $Data::Dumper::Indent = 1;
1561+local $Data::Dumper::Sortkeys = 1;
1562+local $Data::Dumper::Quotekeys = 0;
1563+
1564+use Digest::MD5 qw(md5_hex);
1565+use Sys::Hostname qw(hostname);
1566+use File::Basename qw();
1567+use File::Spec;
1568+use FindBin qw();
1569+
1570+eval {
1571+ require Percona::Toolkit;
1572+ require HTTPMicro;
1573+};
1574+
1575+{
1576+ my $file = 'percona-version-check';
1577+ my $home = $ENV{HOME} || $ENV{HOMEPATH} || $ENV{USERPROFILE} || '.';
1578+ my @vc_dirs = (
1579+ '/etc/percona',
1580+ '/etc/percona-toolkit',
1581+ '/tmp',
1582+ "$home",
1583+ );
1584+
1585+ sub version_check_file {
1586+ foreach my $dir ( @vc_dirs ) {
1587+ if ( -d $dir && -w $dir ) {
1588+ PTDEBUG && _d('Version check file', $file, 'in', $dir);
1589+ return $dir . '/' . $file;
1590+ }
1591+ }
1592+ PTDEBUG && _d('Version check file', $file, 'in', $ENV{PWD});
1593+ return $file; # in the CWD
1594+ }
1595+}
1596+
1597+sub version_check_time_limit {
1598+ return 60 * 60 * 24; # one day
1599+}
1600+
1601+
1602+sub version_check {
1603+ my (%args) = @_;
1604+
1605+ my $instances = $args{instances} || [];
1606+ my $instances_to_check;
1607+
1608+ PTDEBUG && _d('FindBin::Bin:', $FindBin::Bin);
1609+ if ( !$args{force} ) {
1610+ if ( $FindBin::Bin
1611+ && (-d "$FindBin::Bin/../.bzr" || -d "$FindBin::Bin/../../.bzr") ) {
1612+ PTDEBUG && _d("$FindBin::Bin/../.bzr disables --version-check");
1613+ return;
1614+ }
1615+ }
1616+
1617+ eval {
1618+ foreach my $instance ( @$instances ) {
1619+ my ($name, $id) = get_instance_id($instance);
1620+ $instance->{name} = $name;
1621+ $instance->{id} = $id;
1622+ }
1623+
1624+ push @$instances, { name => 'system', id => 0 };
1625+
1626+ $instances_to_check = get_instances_to_check(
1627+ instances => $instances,
1628+ vc_file => $args{vc_file}, # testing
1629+ now => $args{now}, # testing
1630+ );
1631+ PTDEBUG && _d(scalar @$instances_to_check, 'instances to check');
1632+ return unless @$instances_to_check;
1633+
1634+ my $protocol = 'https'; # optimistic, but...
1635+ eval { require IO::Socket::SSL; };
1636+ if ( $EVAL_ERROR ) {
1637+ PTDEBUG && _d($EVAL_ERROR);
1638+ $protocol = 'http';
1639+ }
1640+ PTDEBUG && _d('Using', $protocol);
1641+
1642+ my $advice = pingback(
1643+ instances => $instances_to_check,
1644+ protocol => $protocol,
1645+ url => $args{url} # testing
1646+ || $ENV{PERCONA_VERSION_CHECK_URL} # testing
1647+ || "$protocol://v.percona.com",
1648+ );
1649+ if ( $advice ) {
1650+ PTDEBUG && _d('Advice:', Dumper($advice));
1651+ if ( scalar @$advice > 1) {
1652+ print "\n# " . scalar @$advice . " software updates are "
1653+ . "available:\n";
1654+ }
1655+ else {
1656+ print "\n# A software update is available:\n";
1657+ }
1658+ print join("\n", map { "# * $_" } @$advice), "\n\n";
1659+ }
1660+ };
1661+ if ( $EVAL_ERROR ) {
1662+ PTDEBUG && _d('Version check failed:', $EVAL_ERROR);
1663+ }
1664+
1665+ if ( @$instances_to_check ) {
1666+ eval {
1667+ update_check_times(
1668+ instances => $instances_to_check,
1669+ vc_file => $args{vc_file}, # testing
1670+ now => $args{now}, # testing
1671+ );
1672+ };
1673+ if ( $EVAL_ERROR ) {
1674+ PTDEBUG && _d('Error updating version check file:', $EVAL_ERROR);
1675+ }
1676+ }
1677+
1678+ if ( $ENV{PTDEBUG_VERSION_CHECK} ) {
1679+ warn "Exiting because the PTDEBUG_VERSION_CHECK "
1680+ . "environment variable is defined.\n";
1681+ exit 255;
1682+ }
1683+
1684+ return;
1685+}
1686+
1687+sub get_instances_to_check {
1688+ my (%args) = @_;
1689+
1690+ my $instances = $args{instances};
1691+ my $now = $args{now} || int(time);
1692+ my $vc_file = $args{vc_file} || version_check_file();
1693+
1694+ if ( !-f $vc_file ) {
1695+ PTDEBUG && _d('Version check file', $vc_file, 'does not exist;',
1696+ 'version checking all instances');
1697+ return $instances;
1698+ }
1699+
1700+ open my $fh, '<', $vc_file or die "Cannot open $vc_file: $OS_ERROR";
1701+ chomp(my $file_contents = do { local $/ = undef; <$fh> });
1702+ PTDEBUG && _d('Version check file', $vc_file, 'contents:', $file_contents);
1703+ close $fh;
1704+ my %last_check_time_for = $file_contents =~ /^([^,]+),(.+)$/mg;
1705+
1706+ my $check_time_limit = version_check_time_limit();
1707+ my @instances_to_check;
1708+ foreach my $instance ( @$instances ) {
1709+ my $last_check_time = $last_check_time_for{ $instance->{id} };
1710+ PTDEBUG && _d('Intsance', $instance->{id}, 'last checked',
1711+ $last_check_time, 'now', $now, 'diff', $now - ($last_check_time || 0),
1712+ 'hours until next check',
1713+ sprintf '%.2f',
1714+ ($check_time_limit - ($now - ($last_check_time || 0))) / 3600);
1715+ if ( !defined $last_check_time
1716+ || ($now - $last_check_time) >= $check_time_limit ) {
1717+ PTDEBUG && _d('Time to check', Dumper($instance));
1718+ push @instances_to_check, $instance;
1719+ }
1720+ }
1721+
1722+ return \@instances_to_check;
1723+}
1724+
1725+sub update_check_times {
1726+ my (%args) = @_;
1727+
1728+ my $instances = $args{instances};
1729+ my $now = $args{now} || int(time);
1730+ my $vc_file = $args{vc_file} || version_check_file();
1731+ PTDEBUG && _d('Updating last check time:', $now);
1732+
1733+ open my $fh, '>', $vc_file or die "Cannot write to $vc_file: $OS_ERROR";
1734+ foreach my $instance ( sort { $a->{id} cmp $b->{id} } @$instances ) {
1735+ PTDEBUG && _d('Updated:', Dumper($instance));
1736+ print { $fh } $instance->{id} . ',' . $now . "\n";
1737+ }
1738+ close $fh;
1739+
1740+ return;
1741+}
1742+
1743+sub get_instance_id {
1744+ my ($instance) = @_;
1745+
1746+ my $dbh = $instance->{dbh};
1747+ my $dsn = $instance->{dsn};
1748+
1749+ my $sql = q{SELECT CONCAT(@@hostname, @@port)};
1750+ PTDEBUG && _d($sql);
1751+ my ($name) = eval { $dbh->selectrow_array($sql) };
1752+ if ( $EVAL_ERROR ) {
1753+ PTDEBUG && _d($EVAL_ERROR);
1754+ $sql = q{SELECT @@hostname};
1755+ PTDEBUG && _d($sql);
1756+ ($name) = eval { $dbh->selectrow_array($sql) };
1757+ if ( $EVAL_ERROR ) {
1758+ PTDEBUG && _d($EVAL_ERROR);
1759+ $name = ($dsn->{h} || 'localhost') . ($dsn->{P} || 3306);
1760+ }
1761+ else {
1762+ $sql = q{SHOW VARIABLES LIKE 'port'};
1763+ PTDEBUG && _d($sql);
1764+ my (undef, $port) = eval { $dbh->selectrow_array($sql) };
1765+ PTDEBUG && _d('port:', $port);
1766+ $name .= $port || '';
1767+ }
1768+ }
1769+ my $id = md5_hex($name);
1770+
1771+ PTDEBUG && _d('MySQL instance:', $id, $name, $dsn);
1772+
1773+ return $name, $id;
1774+}
1775+
1776+
1777+sub pingback {
1778+ my (%args) = @_;
1779+ my @required_args = qw(url instances);
1780+ foreach my $arg ( @required_args ) {
1781+ die "I need a $arg arugment" unless $args{$arg};
1782+ }
1783+ my $url = $args{url};
1784+ my $instances = $args{instances};
1785+
1786+ my $ua = $args{ua} || HTTPMicro->new( timeout => 3 );
1787+
1788+ my $response = $ua->request('GET', $url);
1789+ PTDEBUG && _d('Server response:', Dumper($response));
1790+ die "No response from GET $url"
1791+ if !$response;
1792+ die("GET on $url returned HTTP status $response->{status}; expected 200\n",
1793+ ($response->{content} || '')) if $response->{status} != 200;
1794+ die("GET on $url did not return any programs to check")
1795+ if !$response->{content};
1796+
1797+ my $items = parse_server_response(
1798+ response => $response->{content}
1799+ );
1800+ die "Failed to parse server requested programs: $response->{content}"
1801+ if !scalar keys %$items;
1802+
1803+ my $versions = get_versions(
1804+ items => $items,
1805+ instances => $instances,
1806+ );
1807+ die "Failed to get any program versions; should have at least gotten Perl"
1808+ if !scalar keys %$versions;
1809+
1810+ my $client_content = encode_client_response(
1811+ items => $items,
1812+ versions => $versions,
1813+ general_id => md5_hex( hostname() ),
1814+ );
1815+
1816+ my $client_response = {
1817+ headers => { "X-Percona-Toolkit-Tool" => File::Basename::basename($0) },
1818+ content => $client_content,
1819+ };
1820+ PTDEBUG && _d('Client response:', Dumper($client_response));
1821+
1822+ $response = $ua->request('POST', $url, $client_response);
1823+ PTDEBUG && _d('Server suggestions:', Dumper($response));
1824+ die "No response from POST $url $client_response"
1825+ if !$response;
1826+ die "POST $url returned HTTP status $response->{status}; expected 200"
1827+ if $response->{status} != 200;
1828+
1829+ return unless $response->{content};
1830+
1831+ $items = parse_server_response(
1832+ response => $response->{content},
1833+ split_vars => 0,
1834+ );
1835+ die "Failed to parse server suggestions: $response->{content}"
1836+ if !scalar keys %$items;
1837+ my @suggestions = map { $_->{vars} }
1838+ sort { $a->{item} cmp $b->{item} }
1839+ values %$items;
1840+
1841+ return \@suggestions;
1842+}
1843+
1844+sub encode_client_response {
1845+ my (%args) = @_;
1846+ my @required_args = qw(items versions general_id);
1847+ foreach my $arg ( @required_args ) {
1848+ die "I need a $arg arugment" unless $args{$arg};
1849+ }
1850+ my ($items, $versions, $general_id) = @args{@required_args};
1851+
1852+ my @lines;
1853+ foreach my $item ( sort keys %$items ) {
1854+ next unless exists $versions->{$item};
1855+ if ( ref($versions->{$item}) eq 'HASH' ) {
1856+ my $mysql_versions = $versions->{$item};
1857+ for my $id ( sort keys %$mysql_versions ) {
1858+ push @lines, join(';', $id, $item, $mysql_versions->{$id});
1859+ }
1860+ }
1861+ else {
1862+ push @lines, join(';', $general_id, $item, $versions->{$item});
1863+ }
1864+ }
1865+
1866+ my $client_response = join("\n", @lines) . "\n";
1867+ return $client_response;
1868+}
1869+
1870+sub parse_server_response {
1871+ my (%args) = @_;
1872+ my @required_args = qw(response);
1873+ foreach my $arg ( @required_args ) {
1874+ die "I need a $arg arugment" unless $args{$arg};
1875+ }
1876+ my ($response) = @args{@required_args};
1877+
1878+ my %items = map {
1879+ my ($item, $type, $vars) = split(";", $_);
1880+ if ( !defined $args{split_vars} || $args{split_vars} ) {
1881+ $vars = [ split(",", ($vars || '')) ];
1882+ }
1883+ $item => {
1884+ item => $item,
1885+ type => $type,
1886+ vars => $vars,
1887+ };
1888+ } split("\n", $response);
1889+
1890+ PTDEBUG && _d('Items:', Dumper(\%items));
1891+
1892+ return \%items;
1893+}
1894+
1895+my %sub_for_type = (
1896+ os_version => \&get_os_version,
1897+ perl_version => \&get_perl_version,
1898+ perl_module_version => \&get_perl_module_version,
1899+ mysql_variable => \&get_mysql_variable,
1900+ bin_version => \&get_bin_version,
1901+);
1902+
1903+sub valid_item {
1904+ my ($item) = @_;
1905+ return unless $item;
1906+ if ( !exists $sub_for_type{ $item->{type} } ) {
1907+ PTDEBUG && _d('Invalid type:', $item->{type});
1908+ return 0;
1909+ }
1910+ return 1;
1911+}
1912+
1913+sub get_versions {
1914+ my (%args) = @_;
1915+ my @required_args = qw(items);
1916+ foreach my $arg ( @required_args ) {
1917+ die "I need a $arg arugment" unless $args{$arg};
1918+ }
1919+ my ($items) = @args{@required_args};
1920+
1921+ my %versions;
1922+ foreach my $item ( values %$items ) {
1923+ next unless valid_item($item);
1924+ eval {
1925+ my $version = $sub_for_type{ $item->{type} }->(
1926+ item => $item,
1927+ instances => $args{instances},
1928+ );
1929+ if ( $version ) {
1930+ chomp $version unless ref($version);
1931+ $versions{$item->{item}} = $version;
1932+ }
1933+ };
1934+ if ( $EVAL_ERROR ) {
1935+ PTDEBUG && _d('Error getting version for', Dumper($item), $EVAL_ERROR);
1936+ }
1937+ }
1938+
1939+ return \%versions;
1940+}
1941+
1942+
1943+sub get_os_version {
1944+ if ( $OSNAME eq 'MSWin32' ) {
1945+ require Win32;
1946+ return Win32::GetOSDisplayName();
1947+ }
1948+
1949+ chomp(my $platform = `uname -s`);
1950+ PTDEBUG && _d('platform:', $platform);
1951+ return $OSNAME unless $platform;
1952+
1953+ chomp(my $lsb_release
1954+ = `which lsb_release 2>/dev/null | awk '{print \$1}'` || '');
1955+ PTDEBUG && _d('lsb_release:', $lsb_release);
1956+
1957+ my $release = "";
1958+
1959+ if ( $platform eq 'Linux' ) {
1960+ if ( -f "/etc/fedora-release" ) {
1961+ $release = `cat /etc/fedora-release`;
1962+ }
1963+ elsif ( -f "/etc/redhat-release" ) {
1964+ $release = `cat /etc/redhat-release`;
1965+ }
1966+ elsif ( -f "/etc/system-release" ) {
1967+ $release = `cat /etc/system-release`;
1968+ }
1969+ elsif ( $lsb_release ) {
1970+ $release = `$lsb_release -ds`;
1971+ }
1972+ elsif ( -f "/etc/lsb-release" ) {
1973+ $release = `grep DISTRIB_DESCRIPTION /etc/lsb-release`;
1974+ $release =~ s/^\w+="([^"]+)".+/$1/;
1975+ }
1976+ elsif ( -f "/etc/debian_version" ) {
1977+ chomp(my $rel = `cat /etc/debian_version`);
1978+ $release = "Debian $rel";
1979+ if ( -f "/etc/apt/sources.list" ) {
1980+ chomp(my $code_name = `awk '/^deb/ {print \$3}' /etc/apt/sources.list | awk -F/ '{print \$1}'| awk 'BEGIN {FS="|"} {print \$1}' | sort | uniq -c | sort -rn | head -n1 | awk '{print \$2}'`);
1981+ $release .= " ($code_name)" if $code_name;
1982+ }
1983+ }
1984+ elsif ( -f "/etc/os-release" ) { # openSUSE
1985+ chomp($release = `grep PRETTY_NAME /etc/os-release`);
1986+ $release =~ s/^PRETTY_NAME="(.+)"$/$1/;
1987+ }
1988+ elsif ( `ls /etc/*release 2>/dev/null` ) {
1989+ if ( `grep DISTRIB_DESCRIPTION /etc/*release 2>/dev/null` ) {
1990+ $release = `grep DISTRIB_DESCRIPTION /etc/*release | head -n1`;
1991+ }
1992+ else {
1993+ $release = `cat /etc/*release | head -n1`;
1994+ }
1995+ }
1996+ }
1997+ elsif ( $platform =~ m/(?:BSD|^Darwin)$/ ) {
1998+ my $rel = `uname -r`;
1999+ $release = "$platform $rel";
2000+ }
2001+ elsif ( $platform eq "SunOS" ) {
2002+ my $rel = `head -n1 /etc/release` || `uname -r`;
2003+ $release = "$platform $rel";
2004+ }
2005+
2006+ if ( !$release ) {
2007+ PTDEBUG && _d('Failed to get the release, using platform');
2008+ $release = $platform;
2009+ }
2010+ chomp($release);
2011+
2012+ $release =~ s/^"|"$//g;
2013+
2014+ PTDEBUG && _d('OS version =', $release);
2015+ return $release;
2016+}
2017+
2018+sub get_perl_version {
2019+ my (%args) = @_;
2020+ my $item = $args{item};
2021+ return unless $item;
2022+
2023+ my $version = sprintf '%vd', $PERL_VERSION;
2024+ PTDEBUG && _d('Perl version', $version);
2025+ return $version;
2026+}
2027+
2028+sub get_perl_module_version {
2029+ my (%args) = @_;
2030+ my $item = $args{item};
2031+ return unless $item;
2032+
2033+ my $var = '$' . $item->{item} . '::VERSION';
2034+ my $version = eval "use $item->{item}; $var;";
2035+ PTDEBUG && _d('Perl version for', $var, '=', $version);
2036+ return $version;
2037+}
2038+
2039+sub get_mysql_variable {
2040+ return get_from_mysql(
2041+ show => 'VARIABLES',
2042+ @_,
2043+ );
2044+}
2045+
2046+sub get_from_mysql {
2047+ my (%args) = @_;
2048+ my $show = $args{show};
2049+ my $item = $args{item};
2050+ my $instances = $args{instances};
2051+ return unless $show && $item;
2052+
2053+ if ( !$instances || !@$instances ) {
2054+ PTDEBUG && _d('Cannot check', $item,
2055+ 'because there are no MySQL instances');
2056+ return;
2057+ }
2058+
2059+ my @versions;
2060+ my %version_for;
2061+ foreach my $instance ( @$instances ) {
2062+ next unless $instance->{id}; # special system instance has id=0
2063+ my $dbh = $instance->{dbh};
2064+ local $dbh->{FetchHashKeyName} = 'NAME_lc';
2065+ my $sql = qq/SHOW $show/;
2066+ PTDEBUG && _d($sql);
2067+ my $rows = $dbh->selectall_hashref($sql, 'variable_name');
2068+
2069+ my @versions;
2070+ foreach my $var ( @{$item->{vars}} ) {
2071+ $var = lc($var);
2072+ my $version = $rows->{$var}->{value};
2073+ PTDEBUG && _d('MySQL version for', $item->{item}, '=', $version,
2074+ 'on', $instance->{name});
2075+ push @versions, $version;
2076+ }
2077+ $version_for{ $instance->{id} } = join(' ', @versions);
2078+ }
2079+
2080+ return \%version_for;
2081+}
2082+
2083+sub get_bin_version {
2084+ my (%args) = @_;
2085+ my $item = $args{item};
2086+ my $cmd = $item->{item};
2087+ return unless $cmd;
2088+
2089+ my $sanitized_command = File::Basename::basename($cmd);
2090+ PTDEBUG && _d('cmd:', $cmd, 'sanitized:', $sanitized_command);
2091+ return if $sanitized_command !~ /\A[a-zA-Z0-9_-]+\z/;
2092+
2093+ my $output = `$sanitized_command --version 2>&1`;
2094+ PTDEBUG && _d('output:', $output);
2095+
2096+ my ($version) = $output =~ /v?([0-9]+\.[0-9]+(?:\.[\w-]+)?)/;
2097+
2098+ PTDEBUG && _d('Version for', $sanitized_command, '=', $version);
2099+ return $version;
2100+}
2101+
2102+sub _d {
2103+ my ($package, undef, $line) = caller 0;
2104+ @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
2105+ map { defined $_ ? $_ : 'undef' }
2106+ @_;
2107+ print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
2108+}
2109+
2110+1;
2111+}
2112+# ###########################################################################
2113+# End VersionCheck package
2114+# ###########################################################################
2115+
2116+# ###########################################################################
2117+# DSNParser package
2118+# This package is a copy without comments from the original. The original
2119+# with comments and its test file can be found in the Bazaar repository at,
2120+# lib/DSNParser.pm
2121+# t/lib/DSNParser.t
2122+# See https://launchpad.net/percona-toolkit for more information.
2123+# ###########################################################################
2124+{
2125+package DSNParser;
2126+
2127+use strict;
2128+use warnings FATAL => 'all';
2129+use English qw(-no_match_vars);
2130+use constant PTDEBUG => $ENV{PTDEBUG} || 0;
2131+
2132+use Data::Dumper;
2133+$Data::Dumper::Indent = 0;
2134+$Data::Dumper::Quotekeys = 0;
2135+
2136+my $dsn_sep = qr/(?<!\\),/;
2137+
2138+eval {
2139+ require DBI;
2140+};
2141+my $have_dbi = $EVAL_ERROR ? 0 : 1;
2142+
2143+sub new {
2144+ my ( $class, %args ) = @_;
2145+ foreach my $arg ( qw(opts) ) {
2146+ die "I need a $arg argument" unless $args{$arg};
2147+ }
2148+ my $self = {
2149+ opts => {} # h, P, u, etc. Should come from DSN OPTIONS section in POD.
2150+ };
2151+ foreach my $opt ( @{$args{opts}} ) {
2152+ if ( !$opt->{key} || !$opt->{desc} ) {
2153+ die "Invalid DSN option: ", Dumper($opt);
2154+ }
2155+ PTDEBUG && _d('DSN option:',
2156+ join(', ',
2157+ map { "$_=" . (defined $opt->{$_} ? ($opt->{$_} || '') : 'undef') }
2158+ keys %$opt
2159+ )
2160+ );
2161+ $self->{opts}->{$opt->{key}} = {
2162+ dsn => $opt->{dsn},
2163+ desc => $opt->{desc},
2164+ copy => $opt->{copy} || 0,
2165+ };
2166+ }
2167+ return bless $self, $class;
2168+}
2169+
2170+sub prop {
2171+ my ( $self, $prop, $value ) = @_;
2172+ if ( @_ > 2 ) {
2173+ PTDEBUG && _d('Setting', $prop, 'property');
2174+ $self->{$prop} = $value;
2175+ }
2176+ return $self->{$prop};
2177+}
2178+
2179+sub parse {
2180+ my ( $self, $dsn, $prev, $defaults ) = @_;
2181+ if ( !$dsn ) {
2182+ PTDEBUG && _d('No DSN to parse');
2183+ return;
2184+ }
2185+ PTDEBUG && _d('Parsing', $dsn);
2186+ $prev ||= {};
2187+ $defaults ||= {};
2188+ my %given_props;
2189+ my %final_props;
2190+ my $opts = $self->{opts};
2191+
2192+ foreach my $dsn_part ( split($dsn_sep, $dsn) ) {
2193+ $dsn_part =~ s/\\,/,/g;
2194+ if ( my ($prop_key, $prop_val) = $dsn_part =~ m/^(.)=(.*)$/ ) {
2195+ $given_props{$prop_key} = $prop_val;
2196+ }
2197+ else {
2198+ PTDEBUG && _d('Interpreting', $dsn_part, 'as h=', $dsn_part);
2199+ $given_props{h} = $dsn_part;
2200+ }
2201+ }
2202+
2203+ foreach my $key ( keys %$opts ) {
2204+ PTDEBUG && _d('Finding value for', $key);
2205+ $final_props{$key} = $given_props{$key};
2206+ if ( !defined $final_props{$key}
2207+ && defined $prev->{$key} && $opts->{$key}->{copy} )
2208+ {
2209+ $final_props{$key} = $prev->{$key};
2210+ PTDEBUG && _d('Copying value for', $key, 'from previous DSN');
2211+ }
2212+ if ( !defined $final_props{$key} ) {
2213+ $final_props{$key} = $defaults->{$key};
2214+ PTDEBUG && _d('Copying value for', $key, 'from defaults');
2215+ }
2216+ }
2217+
2218+ foreach my $key ( keys %given_props ) {
2219+ die "Unknown DSN option '$key' in '$dsn'. For more details, "
2220+ . "please use the --help option, or try 'perldoc $PROGRAM_NAME' "
2221+ . "for complete documentation."
2222+ unless exists $opts->{$key};
2223+ }
2224+ if ( (my $required = $self->prop('required')) ) {
2225+ foreach my $key ( keys %$required ) {
2226+ die "Missing required DSN option '$key' in '$dsn'. For more details, "
2227+ . "please use the --help option, or try 'perldoc $PROGRAM_NAME' "
2228+ . "for complete documentation."
2229+ unless $final_props{$key};
2230+ }
2231+ }
2232+
2233+ return \%final_props;
2234+}
2235+
2236+sub parse_options {
2237+ my ( $self, $o ) = @_;
2238+ die 'I need an OptionParser object' unless ref $o eq 'OptionParser';
2239+ my $dsn_string
2240+ = join(',',
2241+ map { "$_=".$o->get($_); }
2242+ grep { $o->has($_) && $o->get($_) }
2243+ keys %{$self->{opts}}
2244+ );
2245+ PTDEBUG && _d('DSN string made from options:', $dsn_string);
2246+ return $self->parse($dsn_string);
2247+}
2248+
2249+sub as_string {
2250+ my ( $self, $dsn, $props ) = @_;
2251+ return $dsn unless ref $dsn;
2252+ my @keys = $props ? @$props : sort keys %$dsn;
2253+ return join(',',
2254+ map { "$_=" . ($_ eq 'p' ? '...' : $dsn->{$_}) }
2255+ grep {
2256+ exists $self->{opts}->{$_}
2257+ && exists $dsn->{$_}
2258+ && defined $dsn->{$_}
2259+ } @keys);
2260+}
2261+
2262+sub usage {
2263+ my ( $self ) = @_;
2264+ my $usage
2265+ = "DSN syntax is key=value[,key=value...] Allowable DSN keys:\n\n"
2266+ . " KEY COPY MEANING\n"
2267+ . " === ==== =============================================\n";
2268+ my %opts = %{$self->{opts}};
2269+ foreach my $key ( sort keys %opts ) {
2270+ $usage .= " $key "
2271+ . ($opts{$key}->{copy} ? 'yes ' : 'no ')
2272+ . ($opts{$key}->{desc} || '[No description]')
2273+ . "\n";
2274+ }
2275+ $usage .= "\n If the DSN is a bareword, the word is treated as the 'h' key.\n";
2276+ return $usage;
2277+}
2278+
2279+sub get_cxn_params {
2280+ my ( $self, $info ) = @_;
2281+ my $dsn;
2282+ my %opts = %{$self->{opts}};
2283+ my $driver = $self->prop('dbidriver') || '';
2284+ if ( $driver eq 'Pg' ) {
2285+ $dsn = 'DBI:Pg:dbname=' . ( $info->{D} || '' ) . ';'
2286+ . join(';', map { "$opts{$_}->{dsn}=$info->{$_}" }
2287+ grep { defined $info->{$_} }
2288+ qw(h P));
2289+ }
2290+ else {
2291+ $dsn = 'DBI:mysql:' . ( $info->{D} || '' ) . ';'
2292+ . join(';', map { "$opts{$_}->{dsn}=$info->{$_}" }
2293+ grep { defined $info->{$_} }
2294+ qw(F h P S A))
2295+ . ';mysql_read_default_group=client'
2296+ . ($info->{L} ? ';mysql_local_infile=1' : '');
2297+ }
2298+ PTDEBUG && _d($dsn);
2299+ return ($dsn, $info->{u}, $info->{p});
2300+}
2301+
2302+sub fill_in_dsn {
2303+ my ( $self, $dbh, $dsn ) = @_;
2304+ my $vars = $dbh->selectall_hashref('SHOW VARIABLES', 'Variable_name');
2305+ my ($user, $db) = $dbh->selectrow_array('SELECT USER(), DATABASE()');
2306+ $user =~ s/@.*//;
2307+ $dsn->{h} ||= $vars->{hostname}->{Value};
2308+ $dsn->{S} ||= $vars->{'socket'}->{Value};
2309+ $dsn->{P} ||= $vars->{port}->{Value};
2310+ $dsn->{u} ||= $user;
2311+ $dsn->{D} ||= $db;
2312+}
2313+
2314+sub get_dbh {
2315+ my ( $self, $cxn_string, $user, $pass, $opts ) = @_;
2316+ $opts ||= {};
2317+ my $defaults = {
2318+ AutoCommit => 0,
2319+ RaiseError => 1,
2320+ PrintError => 0,
2321+ ShowErrorStatement => 1,
2322+ mysql_enable_utf8 => ($cxn_string =~ m/charset=utf8/i ? 1 : 0),
2323+ };
2324+ @{$defaults}{ keys %$opts } = values %$opts;
2325+ if (delete $defaults->{L}) { # L for LOAD DATA LOCAL INFILE, our own extension
2326+ $defaults->{mysql_local_infile} = 1;
2327+ }
2328+
2329+ if ( $opts->{mysql_use_result} ) {
2330+ $defaults->{mysql_use_result} = 1;
2331+ }
2332+
2333+ if ( !$have_dbi ) {
2334+ die "Cannot connect to MySQL because the Perl DBI module is not "
2335+ . "installed or not found. Run 'perl -MDBI' to see the directories "
2336+ . "that Perl searches for DBI. If DBI is not installed, try:\n"
2337+ . " Debian/Ubuntu apt-get install libdbi-perl\n"
2338+ . " RHEL/CentOS yum install perl-DBI\n"
2339+ . " OpenSolaris pkg install pkg:/SUNWpmdbi\n";
2340+
2341+ }
2342+
2343+ my $dbh;
2344+ my $tries = 2;
2345+ while ( !$dbh && $tries-- ) {
2346+ PTDEBUG && _d($cxn_string, ' ', $user, ' ', $pass,
2347+ join(', ', map { "$_=>$defaults->{$_}" } keys %$defaults ));
2348+
2349+ $dbh = eval { DBI->connect($cxn_string, $user, $pass, $defaults) };
2350+
2351+ if ( !$dbh && $EVAL_ERROR ) {
2352+ if ( $EVAL_ERROR =~ m/locate DBD\/mysql/i ) {
2353+ die "Cannot connect to MySQL because the Perl DBD::mysql module is "
2354+ . "not installed or not found. Run 'perl -MDBD::mysql' to see "
2355+ . "the directories that Perl searches for DBD::mysql. If "
2356+ . "DBD::mysql is not installed, try:\n"
2357+ . " Debian/Ubuntu apt-get install libdbd-mysql-perl\n"
2358+ . " RHEL/CentOS yum install perl-DBD-MySQL\n"
2359+ . " OpenSolaris pgk install pkg:/SUNWapu13dbd-mysql\n";
2360+ }
2361+ elsif ( $EVAL_ERROR =~ m/not a compiled character set|character set utf8/ ) {
2362+ PTDEBUG && _d('Going to try again without utf8 support');
2363+ delete $defaults->{mysql_enable_utf8};
2364+ }
2365+ if ( !$tries ) {
2366+ die $EVAL_ERROR;
2367+ }
2368+ }
2369+ }
2370+
2371+ if ( $cxn_string =~ m/mysql/i ) {
2372+ my $sql;
2373+
2374+ $sql = 'SELECT @@SQL_MODE';
2375+ PTDEBUG && _d($dbh, $sql);
2376+ my ($sql_mode) = eval { $dbh->selectrow_array($sql) };
2377+ if ( $EVAL_ERROR ) {
2378+ die "Error getting the current SQL_MODE: $EVAL_ERROR";
2379+ }
2380+
2381+ if ( my ($charset) = $cxn_string =~ m/charset=([\w]+)/ ) {
2382+ $sql = qq{/*!40101 SET NAMES "$charset"*/};
2383+ PTDEBUG && _d($dbh, $sql);
2384+ eval { $dbh->do($sql) };
2385+ if ( $EVAL_ERROR ) {
2386+ die "Error setting NAMES to $charset: $EVAL_ERROR";
2387+ }
2388+ PTDEBUG && _d('Enabling charset for STDOUT');
2389+ if ( $charset eq 'utf8' ) {
2390+ binmode(STDOUT, ':utf8')
2391+ or die "Can't binmode(STDOUT, ':utf8'): $OS_ERROR";
2392+ }
2393+ else {
2394+ binmode(STDOUT) or die "Can't binmode(STDOUT): $OS_ERROR";
2395+ }
2396+ }
2397+
2398+ if ( my $vars = $self->prop('set-vars') ) {
2399+ $self->set_vars($dbh, $vars);
2400+ }
2401+
2402+ $sql = 'SET @@SQL_QUOTE_SHOW_CREATE = 1'
2403+ . '/*!40101, @@SQL_MODE=\'NO_AUTO_VALUE_ON_ZERO'
2404+ . ($sql_mode ? ",$sql_mode" : '')
2405+ . '\'*/';
2406+ PTDEBUG && _d($dbh, $sql);
2407+ eval { $dbh->do($sql) };
2408+ if ( $EVAL_ERROR ) {
2409+ die "Error setting SQL_QUOTE_SHOW_CREATE, SQL_MODE"
2410+ . ($sql_mode ? " and $sql_mode" : '')
2411+ . ": $EVAL_ERROR";
2412+ }
2413+ }
2414+
2415+ PTDEBUG && _d('DBH info: ',
2416+ $dbh,
2417+ Dumper($dbh->selectrow_hashref(
2418+ 'SELECT DATABASE(), CONNECTION_ID(), VERSION()/*!50038 , @@hostname*/')),
2419+ 'Connection info:', $dbh->{mysql_hostinfo},
2420+ 'Character set info:', Dumper($dbh->selectall_arrayref(
2421+ "SHOW VARIABLES LIKE 'character_set%'", { Slice => {}})),
2422+ '$DBD::mysql::VERSION:', $DBD::mysql::VERSION,
2423+ '$DBI::VERSION:', $DBI::VERSION,
2424+ );
2425+
2426+ return $dbh;
2427+}
2428+
2429+sub get_hostname {
2430+ my ( $self, $dbh ) = @_;
2431+ if ( my ($host) = ($dbh->{mysql_hostinfo} || '') =~ m/^(\w+) via/ ) {
2432+ return $host;
2433+ }
2434+ my ( $hostname, $one ) = $dbh->selectrow_array(
2435+ 'SELECT /*!50038 @@hostname, */ 1');
2436+ return $hostname;
2437+}
2438+
2439+sub disconnect {
2440+ my ( $self, $dbh ) = @_;
2441+ PTDEBUG && $self->print_active_handles($dbh);
2442+ $dbh->disconnect;
2443+}
2444+
2445+sub print_active_handles {
2446+ my ( $self, $thing, $level ) = @_;
2447+ $level ||= 0;
2448+ printf("# Active %sh: %s %s %s\n", ($thing->{Type} || 'undef'), "\t" x $level,
2449+ $thing, (($thing->{Type} || '') eq 'st' ? $thing->{Statement} || '' : ''))
2450+ or die "Cannot print: $OS_ERROR";
2451+ foreach my $handle ( grep {defined} @{ $thing->{ChildHandles} } ) {
2452+ $self->print_active_handles( $handle, $level + 1 );
2453+ }
2454+}
2455+
2456+sub copy {
2457+ my ( $self, $dsn_1, $dsn_2, %args ) = @_;
2458+ die 'I need a dsn_1 argument' unless $dsn_1;
2459+ die 'I need a dsn_2 argument' unless $dsn_2;
2460+ my %new_dsn = map {
2461+ my $key = $_;
2462+ my $val;
2463+ if ( $args{overwrite} ) {
2464+ $val = defined $dsn_1->{$key} ? $dsn_1->{$key} : $dsn_2->{$key};
2465+ }
2466+ else {
2467+ $val = defined $dsn_2->{$key} ? $dsn_2->{$key} : $dsn_1->{$key};
2468+ }
2469+ $key => $val;
2470+ } keys %{$self->{opts}};
2471+ return \%new_dsn;
2472+}
2473+
2474+sub set_vars {
2475+ my ($self, $dbh, $vars) = @_;
2476+
2477+ return unless $vars;
2478+
2479+ foreach my $var ( sort keys %$vars ) {
2480+ my $val = $vars->{$var}->{val};
2481+
2482+ (my $quoted_var = $var) =~ s/_/\\_/;
2483+ my ($var_exists, $current_val);
2484+ eval {
2485+ ($var_exists, $current_val) = $dbh->selectrow_array(
2486+ "SHOW VARIABLES LIKE '$quoted_var'");
2487+ };
2488+ my $e = $EVAL_ERROR;
2489+ if ( $e ) {
2490+ PTDEBUG && _d($e);
2491+ }
2492+
2493+ if ( $vars->{$var}->{default} && !$var_exists ) {
2494+ PTDEBUG && _d('Not setting default var', $var,
2495+ 'because it does not exist');
2496+ next;
2497+ }
2498+
2499+ if ( $current_val && $current_val eq $val ) {
2500+ PTDEBUG && _d('Not setting var', $var, 'because its value',
2501+ 'is already', $val);
2502+ next;
2503+ }
2504+
2505+ my $sql = "SET SESSION $var=$val";
2506+ PTDEBUG && _d($dbh, $sql);
2507+ eval { $dbh->do($sql) };
2508+ if ( my $set_error = $EVAL_ERROR ) {
2509+ chomp($set_error);
2510+ $set_error =~ s/ at \S+ line \d+//;
2511+ my $msg = "Error setting $var: $set_error";
2512+ if ( $current_val ) {
2513+ $msg .= " The current value for $var is $current_val. "
2514+ . "If the variable is read only (not dynamic), specify "
2515+ . "--set-vars $var=$current_val to avoid this warning, "
2516+ . "else manually set the variable and restart MySQL.";
2517+ }
2518+ warn $msg . "\n\n";
2519+ }
2520+ }
2521+
2522+ return;
2523+}
2524+
2525+sub _d {
2526+ my ($package, undef, $line) = caller 0;
2527+ @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
2528+ map { defined $_ ? $_ : 'undef' }
2529+ @_;
2530+ print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
2531+}
2532+
2533+1;
2534+}
2535+# ###########################################################################
2536+# End DSNParser package
2537+# ###########################################################################
2538+
2539+# ###########################################################################
2540+# OptionParser package
2541+# This package is a copy without comments from the original. The original
2542+# with comments and its test file can be found in the Bazaar repository at,
2543+# lib/OptionParser.pm
2544+# t/lib/OptionParser.t
2545+# See https://launchpad.net/percona-toolkit for more information.
2546+# ###########################################################################
2547+{
2548+package OptionParser;
2549+
2550+use strict;
2551+use warnings FATAL => 'all';
2552+use English qw(-no_match_vars);
2553+use constant PTDEBUG => $ENV{PTDEBUG} || 0;
2554+
2555+use List::Util qw(max);
2556+use Getopt::Long;
2557+use Data::Dumper;
2558+
2559+my $POD_link_re = '[LC]<"?([^">]+)"?>';
2560+
2561+sub new {
2562+ my ( $class, %args ) = @_;
2563+ my @required_args = qw();
2564+ foreach my $arg ( @required_args ) {
2565+ die "I need a $arg argument" unless $args{$arg};
2566+ }
2567+
2568+ my ($program_name) = $PROGRAM_NAME =~ m/([.A-Za-z-]+)$/;
2569+ $program_name ||= $PROGRAM_NAME;
2570+ my $home = $ENV{HOME} || $ENV{HOMEPATH} || $ENV{USERPROFILE} || '.';
2571+
2572+ my %attributes = (
2573+ 'type' => 1,
2574+ 'short form' => 1,
2575+ 'group' => 1,
2576+ 'default' => 1,
2577+ 'cumulative' => 1,
2578+ 'negatable' => 1,
2579+ );
2580+
2581+ my $self = {
2582+ head1 => 'OPTIONS', # These args are used internally
2583+ skip_rules => 0, # to instantiate another Option-
2584+ item => '--(.*)', # Parser obj that parses the
2585+ attributes => \%attributes, # DSN OPTIONS section. Tools
2586+ parse_attributes => \&_parse_attribs, # don't tinker with these args.
2587+
2588+ %args,
2589+
2590+ strict => 1, # disabled by a special rule
2591+ program_name => $program_name,
2592+ opts => {},
2593+ got_opts => 0,
2594+ short_opts => {},
2595+ defaults => {},
2596+ groups => {},
2597+ allowed_groups => {},
2598+ errors => [],
2599+ rules => [], # desc of rules for --help
2600+ mutex => [], # rule: opts are mutually exclusive
2601+ atleast1 => [], # rule: at least one opt is required
2602+ disables => {}, # rule: opt disables other opts
2603+ defaults_to => {}, # rule: opt defaults to value of other opt
2604+ DSNParser => undef,
2605+ default_files => [
2606+ "/etc/percona-toolkit/percona-toolkit.conf",
2607+ "/etc/percona-toolkit/$program_name.conf",
2608+ "$home/.percona-toolkit.conf",
2609+ "$home/.$program_name.conf",
2610+ ],
2611+ types => {
2612+ string => 's', # standard Getopt type
2613+ int => 'i', # standard Getopt type
2614+ float => 'f', # standard Getopt type
2615+ Hash => 'H', # hash, formed from a comma-separated list
2616+ hash => 'h', # hash as above, but only if a value is given
2617+ Array => 'A', # array, similar to Hash
2618+ array => 'a', # array, similar to hash
2619+ DSN => 'd', # DSN
2620+ size => 'z', # size with kMG suffix (powers of 2^10)
2621+ time => 'm', # time, with an optional suffix of s/h/m/d
2622+ },
2623+ };
2624+
2625+ return bless $self, $class;
2626+}
2627+
2628+sub get_specs {
2629+ my ( $self, $file ) = @_;
2630+ $file ||= $self->{file} || __FILE__;
2631+ my @specs = $self->_pod_to_specs($file);
2632+ $self->_parse_specs(@specs);
2633+
2634+ open my $fh, "<", $file or die "Cannot open $file: $OS_ERROR";
2635+ my $contents = do { local $/ = undef; <$fh> };
2636+ close $fh;
2637+ if ( $contents =~ m/^=head1 DSN OPTIONS/m ) {
2638+ PTDEBUG && _d('Parsing DSN OPTIONS');
2639+ my $dsn_attribs = {
2640+ dsn => 1,
2641+ copy => 1,
2642+ };
2643+ my $parse_dsn_attribs = sub {
2644+ my ( $self, $option, $attribs ) = @_;
2645+ map {
2646+ my $val = $attribs->{$_};
2647+ if ( $val ) {
2648+ $val = $val eq 'yes' ? 1
2649+ : $val eq 'no' ? 0
2650+ : $val;
2651+ $attribs->{$_} = $val;
2652+ }
2653+ } keys %$attribs;
2654+ return {
2655+ key => $option,
2656+ %$attribs,
2657+ };
2658+ };
2659+ my $dsn_o = new OptionParser(
2660+ description => 'DSN OPTIONS',
2661+ head1 => 'DSN OPTIONS',
2662+ dsn => 0, # XXX don't infinitely recurse!
2663+ item => '\* (.)', # key opts are a single character
2664+ skip_rules => 1, # no rules before opts
2665+ attributes => $dsn_attribs,
2666+ parse_attributes => $parse_dsn_attribs,
2667+ );
2668+ my @dsn_opts = map {
2669+ my $opts = {
2670+ key => $_->{spec}->{key},
2671+ dsn => $_->{spec}->{dsn},
2672+ copy => $_->{spec}->{copy},
2673+ desc => $_->{desc},
2674+ };
2675+ $opts;
2676+ } $dsn_o->_pod_to_specs($file);
2677+ $self->{DSNParser} = DSNParser->new(opts => \@dsn_opts);
2678+ }
2679+
2680+ if ( $contents =~ m/^=head1 VERSION\n\n^(.+)$/m ) {
2681+ $self->{version} = $1;
2682+ PTDEBUG && _d($self->{version});
2683+ }
2684+
2685+ return;
2686+}
2687+
2688+sub DSNParser {
2689+ my ( $self ) = @_;
2690+ return $self->{DSNParser};
2691+};
2692+
2693+sub get_defaults_files {
2694+ my ( $self ) = @_;
2695+ return @{$self->{default_files}};
2696+}
2697+
2698+sub _pod_to_specs {
2699+ my ( $self, $file ) = @_;
2700+ $file ||= $self->{file} || __FILE__;
2701+ open my $fh, '<', $file or die "Cannot open $file: $OS_ERROR";
2702+
2703+ my @specs = ();
2704+ my @rules = ();
2705+ my $para;
2706+
2707+ local $INPUT_RECORD_SEPARATOR = '';
2708+ while ( $para = <$fh> ) {
2709+ next unless $para =~ m/^=head1 $self->{head1}/;
2710+ last;
2711+ }
2712+
2713+ while ( $para = <$fh> ) {
2714+ last if $para =~ m/^=over/;
2715+ next if $self->{skip_rules};
2716+ chomp $para;
2717+ $para =~ s/\s+/ /g;
2718+ $para =~ s/$POD_link_re/$1/go;
2719+ PTDEBUG && _d('Option rule:', $para);
2720+ push @rules, $para;
2721+ }
2722+
2723+ die "POD has no $self->{head1} section" unless $para;
2724+
2725+ do {
2726+ if ( my ($option) = $para =~ m/^=item $self->{item}/ ) {
2727+ chomp $para;
2728+ PTDEBUG && _d($para);
2729+ my %attribs;
2730+
2731+ $para = <$fh>; # read next paragraph, possibly attributes
2732+
2733+ if ( $para =~ m/: / ) { # attributes
2734+ $para =~ s/\s+\Z//g;
2735+ %attribs = map {
2736+ my ( $attrib, $val) = split(/: /, $_);
2737+ die "Unrecognized attribute for --$option: $attrib"
2738+ unless $self->{attributes}->{$attrib};
2739+ ($attrib, $val);
2740+ } split(/; /, $para);
2741+ if ( $attribs{'short form'} ) {
2742+ $attribs{'short form'} =~ s/-//;
2743+ }
2744+ $para = <$fh>; # read next paragraph, probably short help desc
2745+ }
2746+ else {
2747+ PTDEBUG && _d('Option has no attributes');
2748+ }
2749+
2750+ $para =~ s/\s+\Z//g;
2751+ $para =~ s/\s+/ /g;
2752+ $para =~ s/$POD_link_re/$1/go;
2753+
2754+ $para =~ s/\.(?:\n.*| [A-Z].*|\Z)//s;
2755+ PTDEBUG && _d('Short help:', $para);
2756+
2757+ die "No description after option spec $option" if $para =~ m/^=item/;
2758+
2759+ if ( my ($base_option) = $option =~ m/^\[no\](.*)/ ) {
2760+ $option = $base_option;
2761+ $attribs{'negatable'} = 1;
2762+ }
2763+
2764+ push @specs, {
2765+ spec => $self->{parse_attributes}->($self, $option, \%attribs),
2766+ desc => $para
2767+ . (defined $attribs{default} ? " (default $attribs{default})" : ''),
2768+ group => ($attribs{'group'} ? $attribs{'group'} : 'default'),
2769+ };
2770+ }
2771+ while ( $para = <$fh> ) {
2772+ last unless $para;
2773+ if ( $para =~ m/^=head1/ ) {
2774+ $para = undef; # Can't 'last' out of a do {} block.
2775+ last;
2776+ }
2777+ last if $para =~ m/^=item /;
2778+ }
2779+ } while ( $para );
2780+
2781+ die "No valid specs in $self->{head1}" unless @specs;
2782+
2783+ close $fh;
2784+ return @specs, @rules;
2785+}
2786+
2787+sub _parse_specs {
2788+ my ( $self, @specs ) = @_;
2789+ my %disables; # special rule that requires deferred checking
2790+
2791+ foreach my $opt ( @specs ) {
2792+ if ( ref $opt ) { # It's an option spec, not a rule.
2793+ PTDEBUG && _d('Parsing opt spec:',
2794+ map { ($_, '=>', $opt->{$_}) } keys %$opt);
2795+
2796+ my ( $long, $short ) = $opt->{spec} =~ m/^([\w-]+)(?:\|([^!+=]*))?/;
2797+ if ( !$long ) {
2798+ die "Cannot parse long option from spec $opt->{spec}";
2799+ }
2800+ $opt->{long} = $long;
2801+
2802+ die "Duplicate long option --$long" if exists $self->{opts}->{$long};
2803+ $self->{opts}->{$long} = $opt;
2804+
2805+ if ( length $long == 1 ) {
2806+ PTDEBUG && _d('Long opt', $long, 'looks like short opt');
2807+ $self->{short_opts}->{$long} = $long;
2808+ }
2809+
2810+ if ( $short ) {
2811+ die "Duplicate short option -$short"
2812+ if exists $self->{short_opts}->{$short};
2813+ $self->{short_opts}->{$short} = $long;
2814+ $opt->{short} = $short;
2815+ }
2816+ else {
2817+ $opt->{short} = undef;
2818+ }
2819+
2820+ $opt->{is_negatable} = $opt->{spec} =~ m/!/ ? 1 : 0;
2821+ $opt->{is_cumulative} = $opt->{spec} =~ m/\+/ ? 1 : 0;
2822+ $opt->{is_required} = $opt->{desc} =~ m/required/ ? 1 : 0;
2823+
2824+ $opt->{group} ||= 'default';
2825+ $self->{groups}->{ $opt->{group} }->{$long} = 1;
2826+
2827+ $opt->{value} = undef;
2828+ $opt->{got} = 0;
2829+
2830+ my ( $type ) = $opt->{spec} =~ m/=(.)/;
2831+ $opt->{type} = $type;
2832+ PTDEBUG && _d($long, 'type:', $type);
2833+
2834+
2835+ $opt->{spec} =~ s/=./=s/ if ( $type && $type =~ m/[HhAadzm]/ );
2836+
2837+ if ( (my ($def) = $opt->{desc} =~ m/default\b(?: ([^)]+))?/) ) {
2838+ $self->{defaults}->{$long} = defined $def ? $def : 1;
2839+ PTDEBUG && _d($long, 'default:', $def);
2840+ }
2841+
2842+ if ( $long eq 'config' ) {
2843+ $self->{defaults}->{$long} = join(',', $self->get_defaults_files());
2844+ }
2845+
2846+ if ( (my ($dis) = $opt->{desc} =~ m/(disables .*)/) ) {
2847+ $disables{$long} = $dis;
2848+ PTDEBUG && _d('Deferring check of disables rule for', $opt, $dis);
2849+ }
2850+
2851+ $self->{opts}->{$long} = $opt;
2852+ }
2853+ else { # It's an option rule, not a spec.
2854+ PTDEBUG && _d('Parsing rule:', $opt);
2855+ push @{$self->{rules}}, $opt;
2856+ my @participants = $self->_get_participants($opt);
2857+ my $rule_ok = 0;
2858+
2859+ if ( $opt =~ m/mutually exclusive|one and only one/ ) {
2860+ $rule_ok = 1;
2861+ push @{$self->{mutex}}, \@participants;
2862+ PTDEBUG && _d(@participants, 'are mutually exclusive');
2863+ }
2864+ if ( $opt =~ m/at least one|one and only one/ ) {
2865+ $rule_ok = 1;
2866+ push @{$self->{atleast1}}, \@participants;
2867+ PTDEBUG && _d(@participants, 'require at least one');
2868+ }
2869+ if ( $opt =~ m/default to/ ) {
2870+ $rule_ok = 1;
2871+ $self->{defaults_to}->{$participants[0]} = $participants[1];
2872+ PTDEBUG && _d($participants[0], 'defaults to', $participants[1]);
2873+ }
2874+ if ( $opt =~ m/restricted to option groups/ ) {
2875+ $rule_ok = 1;
2876+ my ($groups) = $opt =~ m/groups ([\w\s\,]+)/;
2877+ my @groups = split(',', $groups);
2878+ %{$self->{allowed_groups}->{$participants[0]}} = map {
2879+ s/\s+//;
2880+ $_ => 1;
2881+ } @groups;
2882+ }
2883+ if( $opt =~ m/accepts additional command-line arguments/ ) {
2884+ $rule_ok = 1;
2885+ $self->{strict} = 0;
2886+ PTDEBUG && _d("Strict mode disabled by rule");
2887+ }
2888+
2889+ die "Unrecognized option rule: $opt" unless $rule_ok;
2890+ }
2891+ }
2892+
2893+ foreach my $long ( keys %disables ) {
2894+ my @participants = $self->_get_participants($disables{$long});
2895+ $self->{disables}->{$long} = \@participants;
2896+ PTDEBUG && _d('Option', $long, 'disables', @participants);
2897+ }
2898+
2899+ return;
2900+}
2901+
2902+sub _get_participants {
2903+ my ( $self, $str ) = @_;
2904+ my @participants;
2905+ foreach my $long ( $str =~ m/--(?:\[no\])?([\w-]+)/g ) {
2906+ die "Option --$long does not exist while processing rule $str"
2907+ unless exists $self->{opts}->{$long};
2908+ push @participants, $long;
2909+ }
2910+ PTDEBUG && _d('Participants for', $str, ':', @participants);
2911+ return @participants;
2912+}
2913+
2914+sub opts {
2915+ my ( $self ) = @_;
2916+ my %opts = %{$self->{opts}};
2917+ return %opts;
2918+}
2919+
2920+sub short_opts {
2921+ my ( $self ) = @_;
2922+ my %short_opts = %{$self->{short_opts}};
2923+ return %short_opts;
2924+}
2925+
2926+sub set_defaults {
2927+ my ( $self, %defaults ) = @_;
2928+ $self->{defaults} = {};
2929+ foreach my $long ( keys %defaults ) {
2930+ die "Cannot set default for nonexistent option $long"
2931+ unless exists $self->{opts}->{$long};
2932+ $self->{defaults}->{$long} = $defaults{$long};
2933+ PTDEBUG && _d('Default val for', $long, ':', $defaults{$long});
2934+ }
2935+ return;
2936+}
2937+
2938+sub get_defaults {
2939+ my ( $self ) = @_;
2940+ return $self->{defaults};
2941+}
2942+
2943+sub get_groups {
2944+ my ( $self ) = @_;
2945+ return $self->{groups};
2946+}
2947+
2948+sub _set_option {
2949+ my ( $self, $opt, $val ) = @_;
2950+ my $long = exists $self->{opts}->{$opt} ? $opt
2951+ : exists $self->{short_opts}->{$opt} ? $self->{short_opts}->{$opt}
2952+ : die "Getopt::Long gave a nonexistent option: $opt";
2953+
2954+ $opt = $self->{opts}->{$long};
2955+ if ( $opt->{is_cumulative} ) {
2956+ $opt->{value}++;
2957+ }
2958+ else {
2959+ $opt->{value} = $val;
2960+ }
2961+ $opt->{got} = 1;
2962+ PTDEBUG && _d('Got option', $long, '=', $val);
2963+}
2964+
2965+sub get_opts {
2966+ my ( $self ) = @_;
2967+
2968+ foreach my $long ( keys %{$self->{opts}} ) {
2969+ $self->{opts}->{$long}->{got} = 0;
2970+ $self->{opts}->{$long}->{value}
2971+ = exists $self->{defaults}->{$long} ? $self->{defaults}->{$long}
2972+ : $self->{opts}->{$long}->{is_cumulative} ? 0
2973+ : undef;
2974+ }
2975+ $self->{got_opts} = 0;
2976+
2977+ $self->{errors} = [];
2978+
2979+ if ( @ARGV && $ARGV[0] eq "--config" ) {
2980+ shift @ARGV;
2981+ $self->_set_option('config', shift @ARGV);
2982+ }
2983+ if ( $self->has('config') ) {
2984+ my @extra_args;
2985+ foreach my $filename ( split(',', $self->get('config')) ) {
2986+ eval {
2987+ push @extra_args, $self->_read_config_file($filename);
2988+ };
2989+ if ( $EVAL_ERROR ) {
2990+ if ( $self->got('config') ) {
2991+ die $EVAL_ERROR;
2992+ }
2993+ elsif ( PTDEBUG ) {
2994+ _d($EVAL_ERROR);
2995+ }
2996+ }
2997+ }
2998+ unshift @ARGV, @extra_args;
2999+ }
3000+
3001+ Getopt::Long::Configure('no_ignore_case', 'bundling');
3002+ GetOptions(
3003+ map { $_->{spec} => sub { $self->_set_option(@_); } }
3004+ grep { $_->{long} ne 'config' } # --config is handled specially above.
3005+ values %{$self->{opts}}
3006+ ) or $self->save_error('Error parsing options');
3007+
3008+ if ( exists $self->{opts}->{version} && $self->{opts}->{version}->{got} ) {
3009+ if ( $self->{version} ) {
3010+ print $self->{version}, "\n";
3011+ }
3012+ else {
3013+ print "Error parsing version. See the VERSION section of the tool's documentation.\n";
3014+ }
3015+ exit 1;
3016+ }
3017+
3018+ if ( @ARGV && $self->{strict} ) {
3019+ $self->save_error("Unrecognized command-line options @ARGV");
3020+ }
3021+
3022+ foreach my $mutex ( @{$self->{mutex}} ) {
3023+ my @set = grep { $self->{opts}->{$_}->{got} } @$mutex;
3024+ if ( @set > 1 ) {
3025+ my $err = join(', ', map { "--$self->{opts}->{$_}->{long}" }
3026+ @{$mutex}[ 0 .. scalar(@$mutex) - 2] )
3027+ . ' and --'.$self->{opts}->{$mutex->[-1]}->{long}
3028+ . ' are mutually exclusive.';
3029+ $self->save_error($err);
3030+ }
3031+ }
3032+
3033+ foreach my $required ( @{$self->{atleast1}} ) {
3034+ my @set = grep { $self->{opts}->{$_}->{got} } @$required;
3035+ if ( @set == 0 ) {
3036+ my $err = join(', ', map { "--$self->{opts}->{$_}->{long}" }
3037+ @{$required}[ 0 .. scalar(@$required) - 2] )
3038+ .' or --'.$self->{opts}->{$required->[-1]}->{long};
3039+ $self->save_error("Specify at least one of $err");
3040+ }
3041+ }
3042+
3043+ $self->_check_opts( keys %{$self->{opts}} );
3044+ $self->{got_opts} = 1;
3045+ return;
3046+}
3047+
3048+sub _check_opts {
3049+ my ( $self, @long ) = @_;
3050+ my $long_last = scalar @long;
3051+ while ( @long ) {
3052+ foreach my $i ( 0..$#long ) {
3053+ my $long = $long[$i];
3054+ next unless $long;
3055+ my $opt = $self->{opts}->{$long};
3056+ if ( $opt->{got} ) {
3057+ if ( exists $self->{disables}->{$long} ) {
3058+ my @disable_opts = @{$self->{disables}->{$long}};
3059+ map { $self->{opts}->{$_}->{value} = undef; } @disable_opts;
3060+ PTDEBUG && _d('Unset options', @disable_opts,
3061+ 'because', $long,'disables them');
3062+ }
3063+
3064+ if ( exists $self->{allowed_groups}->{$long} ) {
3065+
3066+ my @restricted_groups = grep {
3067+ !exists $self->{allowed_groups}->{$long}->{$_}
3068+ } keys %{$self->{groups}};
3069+
3070+ my @restricted_opts;
3071+ foreach my $restricted_group ( @restricted_groups ) {
3072+ RESTRICTED_OPT:
3073+ foreach my $restricted_opt (
3074+ keys %{$self->{groups}->{$restricted_group}} )
3075+ {
3076+ next RESTRICTED_OPT if $restricted_opt eq $long;
3077+ push @restricted_opts, $restricted_opt
3078+ if $self->{opts}->{$restricted_opt}->{got};
3079+ }
3080+ }
3081+
3082+ if ( @restricted_opts ) {
3083+ my $err;
3084+ if ( @restricted_opts == 1 ) {
3085+ $err = "--$restricted_opts[0]";
3086+ }
3087+ else {
3088+ $err = join(', ',
3089+ map { "--$self->{opts}->{$_}->{long}" }
3090+ grep { $_ }
3091+ @restricted_opts[0..scalar(@restricted_opts) - 2]
3092+ )
3093+ . ' or --'.$self->{opts}->{$restricted_opts[-1]}->{long};
3094+ }
3095+ $self->save_error("--$long is not allowed with $err");
3096+ }
3097+ }
3098+
3099+ }
3100+ elsif ( $opt->{is_required} ) {
3101+ $self->save_error("Required option --$long must be specified");
3102+ }
3103+
3104+ $self->_validate_type($opt);
3105+ if ( $opt->{parsed} ) {
3106+ delete $long[$i];
3107+ }
3108+ else {
3109+ PTDEBUG && _d('Temporarily failed to parse', $long);
3110+ }
3111+ }
3112+
3113+ die "Failed to parse options, possibly due to circular dependencies"
3114+ if @long == $long_last;
3115+ $long_last = @long;
3116+ }
3117+
3118+ return;
3119+}
3120+
3121+sub _validate_type {
3122+ my ( $self, $opt ) = @_;
3123+ return unless $opt;
3124+
3125+ if ( !$opt->{type} ) {
3126+ $opt->{parsed} = 1;
3127+ return;
3128+ }
3129+
3130+ my $val = $opt->{value};
3131+
3132+ if ( $val && $opt->{type} eq 'm' ) { # type time
3133+ PTDEBUG && _d('Parsing option', $opt->{long}, 'as a time value');
3134+ my ( $prefix, $num, $suffix ) = $val =~ m/([+-]?)(\d+)([a-z])?$/;
3135+ if ( !$suffix ) {
3136+ my ( $s ) = $opt->{desc} =~ m/\(suffix (.)\)/;
3137+ $suffix = $s || 's';
3138+ PTDEBUG && _d('No suffix given; using', $suffix, 'for',
3139+ $opt->{long}, '(value:', $val, ')');
3140+ }
3141+ if ( $suffix =~ m/[smhd]/ ) {
3142+ $val = $suffix eq 's' ? $num # Seconds
3143+ : $suffix eq 'm' ? $num * 60 # Minutes
3144+ : $suffix eq 'h' ? $num * 3600 # Hours
3145+ : $num * 86400; # Days
3146+ $opt->{value} = ($prefix || '') . $val;
3147+ PTDEBUG && _d('Setting option', $opt->{long}, 'to', $val);
3148+ }
3149+ else {
3150+ $self->save_error("Invalid time suffix for --$opt->{long}");
3151+ }
3152+ }
3153+ elsif ( $val && $opt->{type} eq 'd' ) { # type DSN
3154+ PTDEBUG && _d('Parsing option', $opt->{long}, 'as a DSN');
3155+ my $prev = {};
3156+ my $from_key = $self->{defaults_to}->{ $opt->{long} };
3157+ if ( $from_key ) {
3158+ PTDEBUG && _d($opt->{long}, 'DSN copies from', $from_key, 'DSN');
3159+ if ( $self->{opts}->{$from_key}->{parsed} ) {
3160+ $prev = $self->{opts}->{$from_key}->{value};
3161+ }
3162+ else {
3163+ PTDEBUG && _d('Cannot parse', $opt->{long}, 'until',
3164+ $from_key, 'parsed');
3165+ return;
3166+ }
3167+ }
3168+ my $defaults = $self->{DSNParser}->parse_options($self);
3169+ $opt->{value} = $self->{DSNParser}->parse($val, $prev, $defaults);
3170+ }
3171+ elsif ( $val && $opt->{type} eq 'z' ) { # type size
3172+ PTDEBUG && _d('Parsing option', $opt->{long}, 'as a size value');
3173+ $self->_parse_size($opt, $val);
3174+ }
3175+ elsif ( $opt->{type} eq 'H' || (defined $val && $opt->{type} eq 'h') ) {
3176+ $opt->{value} = { map { $_ => 1 } split(/(?<!\\),\s*/, ($val || '')) };
3177+ }
3178+ elsif ( $opt->{type} eq 'A' || (defined $val && $opt->{type} eq 'a') ) {
3179+ $opt->{value} = [ split(/(?<!\\),\s*/, ($val || '')) ];
3180+ }
3181+ else {
3182+ PTDEBUG && _d('Nothing to validate for option',
3183+ $opt->{long}, 'type', $opt->{type}, 'value', $val);
3184+ }
3185+
3186+ $opt->{parsed} = 1;
3187+ return;
3188+}
3189+
3190+sub get {
3191+ my ( $self, $opt ) = @_;
3192+ my $long = (length $opt == 1 ? $self->{short_opts}->{$opt} : $opt);
3193+ die "Option $opt does not exist"
3194+ unless $long && exists $self->{opts}->{$long};
3195+ return $self->{opts}->{$long}->{value};
3196+}
3197+
3198+sub got {
3199+ my ( $self, $opt ) = @_;
3200+ my $long = (length $opt == 1 ? $self->{short_opts}->{$opt} : $opt);
3201+ die "Option $opt does not exist"
3202+ unless $long && exists $self->{opts}->{$long};
3203+ return $self->{opts}->{$long}->{got};
3204+}
3205+
3206+sub has {
3207+ my ( $self, $opt ) = @_;
3208+ my $long = (length $opt == 1 ? $self->{short_opts}->{$opt} : $opt);
3209+ return defined $long ? exists $self->{opts}->{$long} : 0;
3210+}
3211+
3212+sub set {
3213+ my ( $self, $opt, $val ) = @_;
3214+ my $long = (length $opt == 1 ? $self->{short_opts}->{$opt} : $opt);
3215+ die "Option $opt does not exist"
3216+ unless $long && exists $self->{opts}->{$long};
3217+ $self->{opts}->{$long}->{value} = $val;
3218+ return;
3219+}
3220+
3221+sub save_error {
3222+ my ( $self, $error ) = @_;
3223+ push @{$self->{errors}}, $error;
3224+ return;
3225+}
3226+
3227+sub errors {
3228+ my ( $self ) = @_;
3229+ return $self->{errors};
3230+}
3231+
3232+sub usage {
3233+ my ( $self ) = @_;
3234+ warn "No usage string is set" unless $self->{usage}; # XXX
3235+ return "Usage: " . ($self->{usage} || '') . "\n";
3236+}
3237+
3238+sub descr {
3239+ my ( $self ) = @_;
3240+ warn "No description string is set" unless $self->{description}; # XXX
3241+ my $descr = ($self->{description} || $self->{program_name} || '')
3242+ . " For more details, please use the --help option, "
3243+ . "or try 'perldoc $PROGRAM_NAME' "
3244+ . "for complete documentation.";
3245+ $descr = join("\n", $descr =~ m/(.{0,80})(?:\s+|$)/g)
3246+ unless $ENV{DONT_BREAK_LINES};
3247+ $descr =~ s/ +$//mg;
3248+ return $descr;
3249+}
3250+
3251+sub usage_or_errors {
3252+ my ( $self, $file, $return ) = @_;
3253+ $file ||= $self->{file} || __FILE__;
3254+
3255+ if ( !$self->{description} || !$self->{usage} ) {
3256+ PTDEBUG && _d("Getting description and usage from SYNOPSIS in", $file);
3257+ my %synop = $self->_parse_synopsis($file);
3258+ $self->{description} ||= $synop{description};
3259+ $self->{usage} ||= $synop{usage};
3260+ PTDEBUG && _d("Description:", $self->{description},
3261+ "\nUsage:", $self->{usage});
3262+ }
3263+
3264+ if ( $self->{opts}->{help}->{got} ) {
3265+ print $self->print_usage() or die "Cannot print usage: $OS_ERROR";
3266+ exit 0 unless $return;
3267+ }
3268+ elsif ( scalar @{$self->{errors}} ) {
3269+ print $self->print_errors() or die "Cannot print errors: $OS_ERROR";
3270+ exit 1 unless $return;
3271+ }
3272+
3273+ return;
3274+}
3275+
3276+sub print_errors {
3277+ my ( $self ) = @_;
3278+ my $usage = $self->usage() . "\n";
3279+ if ( (my @errors = @{$self->{errors}}) ) {
3280+ $usage .= join("\n * ", 'Errors in command-line arguments:', @errors)
3281+ . "\n";
3282+ }
3283+ return $usage . "\n" . $self->descr();
3284+}
3285+
3286+sub print_usage {
3287+ my ( $self ) = @_;
3288+ die "Run get_opts() before print_usage()" unless $self->{got_opts};
3289+ my @opts = values %{$self->{opts}};
3290+
3291+ my $maxl = max(
3292+ map {
3293+ length($_->{long}) # option long name
3294+ + ($_->{is_negatable} ? 4 : 0) # "[no]" if opt is negatable
3295+ + ($_->{type} ? 2 : 0) # "=x" where x is the opt type
3296+ }
3297+ @opts);
3298+
3299+ my $maxs = max(0,
3300+ map {
3301+ length($_)
3302+ + ($self->{opts}->{$_}->{is_negatable} ? 4 : 0)
3303+ + ($self->{opts}->{$_}->{type} ? 2 : 0)
3304+ }
3305+ values %{$self->{short_opts}});
3306+
3307+ my $lcol = max($maxl, ($maxs + 3));
3308+ my $rcol = 80 - $lcol - 6;
3309+ my $rpad = ' ' x ( 80 - $rcol );
3310+
3311+ $maxs = max($lcol - 3, $maxs);
3312+
3313+ my $usage = $self->descr() . "\n" . $self->usage();
3314+
3315+ my @groups = reverse sort grep { $_ ne 'default'; } keys %{$self->{groups}};
3316+ push @groups, 'default';
3317+
3318+ foreach my $group ( reverse @groups ) {
3319+ $usage .= "\n".($group eq 'default' ? 'Options' : $group).":\n\n";
3320+ foreach my $opt (
3321+ sort { $a->{long} cmp $b->{long} }
3322+ grep { $_->{group} eq $group }
3323+ @opts )
3324+ {
3325+ my $long = $opt->{is_negatable} ? "[no]$opt->{long}" : $opt->{long};
3326+ my $short = $opt->{short};
3327+ my $desc = $opt->{desc};
3328+
3329+ $long .= $opt->{type} ? "=$opt->{type}" : "";
3330+
3331+ if ( $opt->{type} && $opt->{type} eq 'm' ) {
3332+ my ($s) = $desc =~ m/\(suffix (.)\)/;
3333+ $s ||= 's';
3334+ $desc =~ s/\s+\(suffix .\)//;
3335+ $desc .= ". Optional suffix s=seconds, m=minutes, h=hours, "
3336+ . "d=days; if no suffix, $s is used.";
3337+ }
3338+ $desc = join("\n$rpad", grep { $_ } $desc =~ m/(.{0,$rcol}(?!\W))(?:\s+|(?<=\W)|$)/g);
3339+ $desc =~ s/ +$//mg;
3340+ if ( $short ) {
3341+ $usage .= sprintf(" --%-${maxs}s -%s %s\n", $long, $short, $desc);
3342+ }
3343+ else {
3344+ $usage .= sprintf(" --%-${lcol}s %s\n", $long, $desc);
3345+ }
3346+ }
3347+ }
3348+
3349+ $usage .= "\nOption types: s=string, i=integer, f=float, h/H/a/A=comma-separated list, d=DSN, z=size, m=time\n";
3350+
3351+ if ( (my @rules = @{$self->{rules}}) ) {
3352+ $usage .= "\nRules:\n\n";
3353+ $usage .= join("\n", map { " $_" } @rules) . "\n";
3354+ }
3355+ if ( $self->{DSNParser} ) {
3356+ $usage .= "\n" . $self->{DSNParser}->usage();
3357+ }
3358+ $usage .= "\nOptions and values after processing arguments:\n\n";
3359+ foreach my $opt ( sort { $a->{long} cmp $b->{long} } @opts ) {
3360+ my $val = $opt->{value};
3361+ my $type = $opt->{type} || '';
3362+ my $bool = $opt->{spec} =~ m/^[\w-]+(?:\|[\w-])?!?$/;
3363+ $val = $bool ? ( $val ? 'TRUE' : 'FALSE' )
3364+ : !defined $val ? '(No value)'
3365+ : $type eq 'd' ? $self->{DSNParser}->as_string($val)
3366+ : $type =~ m/H|h/ ? join(',', sort keys %$val)
3367+ : $type =~ m/A|a/ ? join(',', @$val)
3368+ : $val;
3369+ $usage .= sprintf(" --%-${lcol}s %s\n", $opt->{long}, $val);
3370+ }
3371+ return $usage;
3372+}
3373+
3374+sub prompt_noecho {
3375+ shift @_ if ref $_[0] eq __PACKAGE__;
3376+ my ( $prompt ) = @_;
3377+ local $OUTPUT_AUTOFLUSH = 1;
3378+ print $prompt
3379+ or die "Cannot print: $OS_ERROR";
3380+ my $response;
3381+ eval {
3382+ require Term::ReadKey;
3383+ Term::ReadKey::ReadMode('noecho');
3384+ chomp($response = <STDIN>);
3385+ Term::ReadKey::ReadMode('normal');
3386+ print "\n"
3387+ or die "Cannot print: $OS_ERROR";
3388+ };
3389+ if ( $EVAL_ERROR ) {
3390+ die "Cannot read response; is Term::ReadKey installed? $EVAL_ERROR";
3391+ }
3392+ return $response;
3393+}
3394+
3395+sub _read_config_file {
3396+ my ( $self, $filename ) = @_;
3397+ open my $fh, "<", $filename or die "Cannot open $filename: $OS_ERROR\n";
3398+ my @args;
3399+ my $prefix = '--';
3400+ my $parse = 1;
3401+
3402+ LINE:
3403+ while ( my $line = <$fh> ) {
3404+ chomp $line;
3405+ next LINE if $line =~ m/^\s*(?:\#|\;|$)/;
3406+ $line =~ s/\s+#.*$//g;
3407+ $line =~ s/^\s+|\s+$//g;
3408+ if ( $line eq '--' ) {
3409+ $prefix = '';
3410+ $parse = 0;
3411+ next LINE;
3412+ }
3413+ if ( $parse
3414+ && (my($opt, $arg) = $line =~ m/^\s*([^=\s]+?)(?:\s*=\s*(.*?)\s*)?$/)
3415+ ) {
3416+ push @args, grep { defined $_ } ("$prefix$opt", $arg);
3417+ }
3418+ elsif ( $line =~ m/./ ) {
3419+ push @args, $line;
3420+ }
3421+ else {
3422+ die "Syntax error in file $filename at line $INPUT_LINE_NUMBER";
3423+ }
3424+ }
3425+ close $fh;
3426+ return @args;
3427+}
3428+
3429+sub read_para_after {
3430+ my ( $self, $file, $regex ) = @_;
3431+ open my $fh, "<", $file or die "Can't open $file: $OS_ERROR";
3432+ local $INPUT_RECORD_SEPARATOR = '';
3433+ my $para;
3434+ while ( $para = <$fh> ) {
3435+ next unless $para =~ m/^=pod$/m;
3436+ last;
3437+ }
3438+ while ( $para = <$fh> ) {
3439+ next unless $para =~ m/$regex/;
3440+ last;
3441+ }
3442+ $para = <$fh>;
3443+ chomp($para);
3444+ close $fh or die "Can't close $file: $OS_ERROR";
3445+ return $para;
3446+}
3447+
3448+sub clone {
3449+ my ( $self ) = @_;
3450+
3451+ my %clone = map {
3452+ my $hashref = $self->{$_};
3453+ my $val_copy = {};
3454+ foreach my $key ( keys %$hashref ) {
3455+ my $ref = ref $hashref->{$key};
3456+ $val_copy->{$key} = !$ref ? $hashref->{$key}
3457+ : $ref eq 'HASH' ? { %{$hashref->{$key}} }
3458+ : $ref eq 'ARRAY' ? [ @{$hashref->{$key}} ]
3459+ : $hashref->{$key};
3460+ }
3461+ $_ => $val_copy;
3462+ } qw(opts short_opts defaults);
3463+
3464+ foreach my $scalar ( qw(got_opts) ) {
3465+ $clone{$scalar} = $self->{$scalar};
3466+ }
3467+
3468+ return bless \%clone;
3469+}
3470+
3471+sub _parse_size {
3472+ my ( $self, $opt, $val ) = @_;
3473+
3474+ if ( lc($val || '') eq 'null' ) {
3475+ PTDEBUG && _d('NULL size for', $opt->{long});
3476+ $opt->{value} = 'null';
3477+ return;
3478+ }
3479+
3480+ my %factor_for = (k => 1_024, M => 1_048_576, G => 1_073_741_824);
3481+ my ($pre, $num, $factor) = $val =~ m/^([+-])?(\d+)([kMG])?$/;
3482+ if ( defined $num ) {
3483+ if ( $factor ) {
3484+ $num *= $factor_for{$factor};
3485+ PTDEBUG && _d('Setting option', $opt->{y},
3486+ 'to num', $num, '* factor', $factor);
3487+ }
3488+ $opt->{value} = ($pre || '') . $num;
3489+ }
3490+ else {
3491+ $self->save_error("Invalid size for --$opt->{long}: $val");
3492+ }
3493+ return;
3494+}
3495+
3496+sub _parse_attribs {
3497+ my ( $self, $option, $attribs ) = @_;
3498+ my $types = $self->{types};
3499+ return $option
3500+ . ($attribs->{'short form'} ? '|' . $attribs->{'short form'} : '' )
3501+ . ($attribs->{'negatable'} ? '!' : '' )
3502+ . ($attribs->{'cumulative'} ? '+' : '' )
3503+ . ($attribs->{'type'} ? '=' . $types->{$attribs->{type}} : '' );
3504+}
3505+
3506+sub _parse_synopsis {
3507+ my ( $self, $file ) = @_;
3508+ $file ||= $self->{file} || __FILE__;
3509+ PTDEBUG && _d("Parsing SYNOPSIS in", $file);
3510+
3511+ local $INPUT_RECORD_SEPARATOR = ''; # read paragraphs
3512+ open my $fh, "<", $file or die "Cannot open $file: $OS_ERROR";
3513+ my $para;
3514+ 1 while defined($para = <$fh>) && $para !~ m/^=head1 SYNOPSIS/;
3515+ die "$file does not contain a SYNOPSIS section" unless $para;
3516+ my @synop;
3517+ for ( 1..2 ) { # 1 for the usage, 2 for the description
3518+ my $para = <$fh>;
3519+ push @synop, $para;
3520+ }
3521+ close $fh;
3522+ PTDEBUG && _d("Raw SYNOPSIS text:", @synop);
3523+ my ($usage, $desc) = @synop;
3524+ die "The SYNOPSIS section in $file is not formatted properly"
3525+ unless $usage && $desc;
3526+
3527+ $usage =~ s/^\s*Usage:\s+(.+)/$1/;
3528+ chomp $usage;
3529+
3530+ $desc =~ s/\n/ /g;
3531+ $desc =~ s/\s{2,}/ /g;
3532+ $desc =~ s/\. ([A-Z][a-z])/. $1/g;
3533+ $desc =~ s/\s+$//;
3534+
3535+ return (
3536+ description => $desc,
3537+ usage => $usage,
3538+ );
3539+};
3540+
3541+sub set_vars {
3542+ my ($self, $file) = @_;
3543+ $file ||= $self->{file} || __FILE__;
3544+
3545+ my %user_vars;
3546+ my $user_vars = $self->has('set-vars') ? $self->get('set-vars') : undef;
3547+ if ( $user_vars ) {
3548+ foreach my $var_val ( @$user_vars ) {
3549+ my ($var, $val) = $var_val =~ m/([^\s=]+)=(\S+)/;
3550+ die "Invalid --set-vars value: $var_val\n" unless $var && $val;
3551+ $user_vars{$var} = {
3552+ val => $val,
3553+ default => 0,
3554+ };
3555+ }
3556+ }
3557+
3558+ my %default_vars;
3559+ my $default_vars = $self->read_para_after($file, qr/MAGIC_set_vars/);
3560+ if ( $default_vars ) {
3561+ %default_vars = map {
3562+ my $var_val = $_;
3563+ my ($var, $val) = $var_val =~ m/([^\s=]+)=(\S+)/;
3564+ die "Invalid --set-vars value: $var_val\n" unless $var && $val;
3565+ $var => {
3566+ val => $val,
3567+ default => 1,
3568+ };
3569+ } split("\n", $default_vars);
3570+ }
3571+
3572+ my %vars = (
3573+ %default_vars, # first the tool's defaults
3574+ %user_vars, # then the user's which overwrite the defaults
3575+ );
3576+ PTDEBUG && _d('--set-vars:', Dumper(\%vars));
3577+ return \%vars;
3578+}
3579+
3580+sub _d {
3581+ my ($package, undef, $line) = caller 0;
3582+ @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
3583+ map { defined $_ ? $_ : 'undef' }
3584+ @_;
3585+ print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
3586+}
3587+
3588+if ( PTDEBUG ) {
3589+ print '# ', $^X, ' ', $], "\n";
3590+ if ( my $uname = `uname -a` ) {
3591+ $uname =~ s/\s+/ /g;
3592+ print "# $uname\n";
3593+ }
3594+ print '# Arguments: ',
3595+ join(' ', map { my $a = "_[$_]_"; $a =~ s/\n/\n# /g; $a; } @ARGV), "\n";
3596+}
3597+
3598+1;
3599+}
3600+# ###########################################################################
3601+# End OptionParser package
3602+# ###########################################################################
3603+
3604+# ###########################################################################
3605+# Cxn package
3606+# This package is a copy without comments from the original. The original
3607+# with comments and its test file can be found in the Bazaar repository at,
3608+# lib/Cxn.pm
3609+# t/lib/Cxn.t
3610+# See https://launchpad.net/percona-toolkit for more information.
3611+# ###########################################################################
3612+{
3613+package Cxn;
3614+
3615+use strict;
3616+use warnings FATAL => 'all';
3617+use English qw(-no_match_vars);
3618+use Scalar::Util qw(blessed);
3619+use constant {
3620+ PTDEBUG => $ENV{PTDEBUG} || 0,
3621+ PERCONA_TOOLKIT_TEST_USE_DSN_NAMES => $ENV{PERCONA_TOOLKIT_TEST_USE_DSN_NAMES} || 0,
3622+};
3623+
3624+sub new {
3625+ my ( $class, %args ) = @_;
3626+ my @required_args = qw(DSNParser OptionParser);
3627+ foreach my $arg ( @required_args ) {
3628+ die "I need a $arg argument" unless $args{$arg};
3629+ };
3630+ my ($dp, $o) = @args{@required_args};
3631+
3632+ my $dsn_defaults = $dp->parse_options($o);
3633+ my $prev_dsn = $args{prev_dsn};
3634+ my $dsn = $args{dsn};
3635+ if ( !$dsn ) {
3636+ $args{dsn_string} ||= 'h=' . ($dsn_defaults->{h} || 'localhost');
3637+
3638+ $dsn = $dp->parse(
3639+ $args{dsn_string}, $prev_dsn, $dsn_defaults);
3640+ }
3641+ elsif ( $prev_dsn ) {
3642+ $dsn = $dp->copy($prev_dsn, $dsn);
3643+ }
3644+
3645+ my $dsn_name = $dp->as_string($dsn, [qw(h P S)])
3646+ || $dp->as_string($dsn, [qw(F)])
3647+ || '';
3648+
3649+ my $self = {
3650+ dsn => $dsn,
3651+ dbh => $args{dbh},
3652+ dsn_name => $dsn_name,
3653+ hostname => '',
3654+ set => $args{set},
3655+ NAME_lc => defined($args{NAME_lc}) ? $args{NAME_lc} : 1,
3656+ dbh_set => 0,
3657+ ask_pass => $args{ask_pass},
3658+ DSNParser => $dp,
3659+ is_cluster_node => undef,
3660+ parent => $args{parent},
3661+ };
3662+
3663+ return bless $self, $class;
3664+}
3665+
3666+sub connect {
3667+ my ( $self, %opts ) = @_;
3668+ my $dsn = $self->{dsn};
3669+ my $dp = $self->{DSNParser};
3670+
3671+ my $dbh = $self->{dbh};
3672+ if ( !$dbh || !$dbh->ping() ) {
3673+ if ( $self->{ask_pass} && !$self->{asked_for_pass} ) {
3674+ $dsn->{p} = OptionParser::prompt_noecho("Enter MySQL password: ");
3675+ $self->{asked_for_pass} = 1;
3676+ }
3677+ $dbh = $dp->get_dbh(
3678+ $dp->get_cxn_params($dsn),
3679+ {
3680+ AutoCommit => 1,
3681+ %opts,
3682+ },
3683+ );
3684+ }
3685+
3686+ $dbh = $self->set_dbh($dbh);
3687+ PTDEBUG && _d($dbh, 'Connected dbh to', $self->{hostname},$self->{dsn_name});
3688+ return $dbh;
3689+}
3690+
3691+sub set_dbh {
3692+ my ($self, $dbh) = @_;
3693+
3694+ if ( $self->{dbh} && $self->{dbh} == $dbh && $self->{dbh_set} ) {
3695+ PTDEBUG && _d($dbh, 'Already set dbh');
3696+ return $dbh;
3697+ }
3698+
3699+ PTDEBUG && _d($dbh, 'Setting dbh');
3700+
3701+ $dbh->{FetchHashKeyName} = 'NAME_lc' if $self->{NAME_lc};
3702+
3703+ my $sql = 'SELECT @@hostname, @@server_id';
3704+ PTDEBUG && _d($dbh, $sql);
3705+ my ($hostname, $server_id) = $dbh->selectrow_array($sql);
3706+ PTDEBUG && _d($dbh, 'hostname:', $hostname, $server_id);
3707+ if ( $hostname ) {
3708+ $self->{hostname} = $hostname;
3709+ }
3710+
3711+ if ( $self->{parent} ) {
3712+ PTDEBUG && _d($dbh, 'Setting InactiveDestroy=1 in parent');
3713+ $dbh->{InactiveDestroy} = 1;
3714+ }
3715+
3716+ if ( my $set = $self->{set}) {
3717+ $set->($dbh);
3718+ }
3719+
3720+ $self->{dbh} = $dbh;
3721+ $self->{dbh_set} = 1;
3722+ return $dbh;
3723+}
3724+
3725+sub lost_connection {
3726+ my ($self, $e) = @_;
3727+ return 0 unless $e;
3728+ return $e =~ m/MySQL server has gone away/
3729+ || $e =~ m/Lost connection to MySQL server/;
3730+}
3731+
3732+sub dbh {
3733+ my ($self) = @_;
3734+ return $self->{dbh};
3735+}
3736+
3737+sub dsn {
3738+ my ($self) = @_;
3739+ return $self->{dsn};
3740+}
3741+
3742+sub name {
3743+ my ($self) = @_;
3744+ return $self->{dsn_name} if PERCONA_TOOLKIT_TEST_USE_DSN_NAMES;
3745+ return $self->{hostname} || $self->{dsn_name} || 'unknown host';
3746+}
3747+
3748+sub DESTROY {
3749+ my ($self) = @_;
3750+
3751+ PTDEBUG && _d('Destroying cxn');
3752+
3753+ if ( $self->{parent} ) {
3754+ PTDEBUG && _d($self->{dbh}, 'Not disconnecting dbh in parent');
3755+ }
3756+ elsif ( $self->{dbh}
3757+ && blessed($self->{dbh})
3758+ && $self->{dbh}->can("disconnect") )
3759+ {
3760+ PTDEBUG && _d($self->{dbh}, 'Disconnecting dbh on', $self->{hostname},
3761+ $self->{dsn_name});
3762+ $self->{dbh}->disconnect();
3763+ }
3764+
3765+ return;
3766+}
3767+
3768+sub _d {
3769+ my ($package, undef, $line) = caller 0;
3770+ @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
3771+ map { defined $_ ? $_ : 'undef' }
3772+ @_;
3773+ print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
3774+}
3775+
3776+1;
3777+}
3778+# ###########################################################################
3779+# End Cxn package
3780+# ###########################################################################
3781+
3782+# ###########################################################################
3783+# Quoter package
3784+# This package is a copy without comments from the original. The original
3785+# with comments and its test file can be found in the Bazaar repository at,
3786+# lib/Quoter.pm
3787+# t/lib/Quoter.t
3788+# See https://launchpad.net/percona-toolkit for more information.
3789+# ###########################################################################
3790+{
3791+package Quoter;
3792+
3793+use strict;
3794+use warnings FATAL => 'all';
3795+use English qw(-no_match_vars);
3796+use constant PTDEBUG => $ENV{PTDEBUG} || 0;
3797+
3798+use Data::Dumper;
3799+$Data::Dumper::Indent = 1;
3800+$Data::Dumper::Sortkeys = 1;
3801+$Data::Dumper::Quotekeys = 0;
3802+
3803+sub new {
3804+ my ( $class, %args ) = @_;
3805+ return bless {}, $class;
3806+}
3807+
3808+sub quote {
3809+ my ( $self, @vals ) = @_;
3810+ foreach my $val ( @vals ) {
3811+ $val =~ s/`/``/g;
3812+ }
3813+ return join('.', map { '`' . $_ . '`' } @vals);
3814+}
3815+
3816+sub quote_val {
3817+ my ( $self, $val, %args ) = @_;
3818+
3819+ return 'NULL' unless defined $val; # undef = NULL
3820+ return "''" if $val eq ''; # blank string = ''
3821+ return $val if $val =~ m/^0x[0-9a-fA-F]+$/ # quote hex data
3822+ && !$args{is_char}; # unless is_char is true
3823+
3824+ $val =~ s/(['\\])/\\$1/g;
3825+ return "'$val'";
3826+}
3827+
3828+sub split_unquote {
3829+ my ( $self, $db_tbl, $default_db ) = @_;
3830+ my ( $db, $tbl ) = split(/[.]/, $db_tbl);
3831+ if ( !$tbl ) {
3832+ $tbl = $db;
3833+ $db = $default_db;
3834+ }
3835+ for ($db, $tbl) {
3836+ next unless $_;
3837+ s/\A`//;
3838+ s/`\z//;
3839+ s/``/`/g;
3840+ }
3841+
3842+ return ($db, $tbl);
3843+}
3844+
3845+sub literal_like {
3846+ my ( $self, $like ) = @_;
3847+ return unless $like;
3848+ $like =~ s/([%_])/\\$1/g;
3849+ return "'$like'";
3850+}
3851+
3852+sub join_quote {
3853+ my ( $self, $default_db, $db_tbl ) = @_;
3854+ return unless $db_tbl;
3855+ my ($db, $tbl) = split(/[.]/, $db_tbl);
3856+ if ( !$tbl ) {
3857+ $tbl = $db;
3858+ $db = $default_db;
3859+ }
3860+ $db = "`$db`" if $db && $db !~ m/^`/;
3861+ $tbl = "`$tbl`" if $tbl && $tbl !~ m/^`/;
3862+ return $db ? "$db.$tbl" : $tbl;
3863+}
3864+
3865+sub serialize_list {
3866+ my ( $self, @args ) = @_;
3867+ PTDEBUG && _d('Serializing', Dumper(\@args));
3868+ return unless @args;
3869+
3870+ my @parts;
3871+ foreach my $arg ( @args ) {
3872+ if ( defined $arg ) {
3873+ $arg =~ s/,/\\,/g; # escape commas
3874+ $arg =~ s/\\N/\\\\N/g; # escape literal \N
3875+ push @parts, $arg;
3876+ }
3877+ else {
3878+ push @parts, '\N';
3879+ }
3880+ }
3881+
3882+ my $string = join(',', @parts);
3883+ PTDEBUG && _d('Serialized: <', $string, '>');
3884+ return $string;
3885+}
3886+
3887+sub deserialize_list {
3888+ my ( $self, $string ) = @_;
3889+ PTDEBUG && _d('Deserializing <', $string, '>');
3890+ die "Cannot deserialize an undefined string" unless defined $string;
3891+
3892+ my @parts;
3893+ foreach my $arg ( split(/(?<!\\),/, $string) ) {
3894+ if ( $arg eq '\N' ) {
3895+ $arg = undef;
3896+ }
3897+ else {
3898+ $arg =~ s/\\,/,/g;
3899+ $arg =~ s/\\\\N/\\N/g;
3900+ }
3901+ push @parts, $arg;
3902+ }
3903+
3904+ if ( !@parts ) {
3905+ my $n_empty_strings = $string =~ tr/,//;
3906+ $n_empty_strings++;
3907+ PTDEBUG && _d($n_empty_strings, 'empty strings');
3908+ map { push @parts, '' } 1..$n_empty_strings;
3909+ }
3910+ elsif ( $string =~ m/(?<!\\),$/ ) {
3911+ PTDEBUG && _d('Last value is an empty string');
3912+ push @parts, '';
3913+ }
3914+
3915+ PTDEBUG && _d('Deserialized', Dumper(\@parts));
3916+ return @parts;
3917+}
3918+
3919+sub _d {
3920+ my ($package, undef, $line) = caller 0;
3921+ @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
3922+ map { defined $_ ? $_ : 'undef' }
3923+ @_;
3924+ print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
3925+}
3926+
3927+1;
3928+}
3929+# ###########################################################################
3930+# End Quoter package
3931+# ###########################################################################
3932+
3933+# ###########################################################################
3934+# VersionParser package
3935+# This package is a copy without comments from the original. The original
3936+# with comments and its test file can be found in the Bazaar repository at,
3937+# lib/VersionParser.pm
3938+# t/lib/VersionParser.t
3939+# See https://launchpad.net/percona-toolkit for more information.
3940+# ###########################################################################
3941+{
3942+package VersionParser;
3943+
3944+use Lmo;
3945+use Scalar::Util qw(blessed);
3946+use English qw(-no_match_vars);
3947+use constant PTDEBUG => $ENV{PTDEBUG} || 0;
3948+
3949+use overload (
3950+ '""' => "version",
3951+ '<=>' => "cmp",
3952+ 'cmp' => "cmp",
3953+ fallback => 1,
3954+);
3955+
3956+use Carp ();
3957+
3958+has major => (
3959+ is => 'ro',
3960+ isa => 'Int',
3961+ required => 1,
3962+);
3963+
3964+has [qw( minor revision )] => (
3965+ is => 'ro',
3966+ isa => 'Num',
3967+);
3968+
3969+has flavor => (
3970+ is => 'ro',
3971+ isa => 'Str',
3972+ default => sub { 'Unknown' },
3973+);
3974+
3975+has innodb_version => (
3976+ is => 'ro',
3977+ isa => 'Str',
3978+ default => sub { 'NO' },
3979+);
3980+
3981+sub series {
3982+ my $self = shift;
3983+ return $self->_join_version($self->major, $self->minor);
3984+}
3985+
3986+sub version {
3987+ my $self = shift;
3988+ return $self->_join_version($self->major, $self->minor, $self->revision);
3989+}
3990+
3991+sub is_in {
3992+ my ($self, $target) = @_;
3993+
3994+ return $self eq $target;
3995+}
3996+
3997+sub _join_version {
3998+ my ($self, @parts) = @_;
3999+
4000+ return join ".", map { my $c = $_; $c =~ s/^0\./0/; $c } grep defined, @parts;
4001+}
4002+sub _split_version {
4003+ my ($self, $str) = @_;
4004+ my @version_parts = map { s/^0(?=\d)/0./; $_ } $str =~ m/(\d+)/g;
4005+ return @version_parts[0..2];
4006+}
4007+
4008+sub normalized_version {
4009+ my ( $self ) = @_;
4010+ my $result = sprintf('%d%02d%02d', map { $_ || 0 } $self->major,
4011+ $self->minor,
4012+ $self->revision);
4013+ PTDEBUG && _d($self->version, 'normalizes to', $result);
4014+ return $result;
4015+}
4016+
4017+sub comment {
4018+ my ( $self, $cmd ) = @_;
4019+ my $v = $self->normalized_version();
4020+
4021+ return "/*!$v $cmd */"
4022+}
4023+
4024+my @methods = qw(major minor revision);
4025+sub cmp {
4026+ my ($left, $right) = @_;
4027+ my $right_obj = (blessed($right) && $right->isa(ref($left)))
4028+ ? $right
4029+ : ref($left)->new($right);
4030+
4031+ my $retval = 0;
4032+ for my $m ( @methods ) {
4033+ last unless defined($left->$m) && defined($right_obj->$m);
4034+ $retval = $left->$m <=> $right_obj->$m;
4035+ last if $retval;
4036+ }
4037+ return $retval;
4038+}
4039+
4040+sub BUILDARGS {
4041+ my $self = shift;
4042+
4043+ if ( @_ == 1 ) {
4044+ my %args;
4045+ if ( blessed($_[0]) && $_[0]->can("selectrow_hashref") ) {
4046+ PTDEBUG && _d("VersionParser got a dbh, trying to get the version");
4047+ my $dbh = $_[0];
4048+ local $dbh->{FetchHashKeyName} = 'NAME_lc';
4049+ my $query = eval {
4050+ $dbh->selectall_arrayref(q/SHOW VARIABLES LIKE 'version%'/, { Slice => {} })
4051+ };
4052+ if ( $query ) {
4053+ $query = { map { $_->{variable_name} => $_->{value} } @$query };
4054+ @args{@methods} = $self->_split_version($query->{version});
4055+ $args{flavor} = delete $query->{version_comment}
4056+ if $query->{version_comment};
4057+ }
4058+ elsif ( eval { ($query) = $dbh->selectrow_array(q/SELECT VERSION()/) } ) {
4059+ @args{@methods} = $self->_split_version($query);
4060+ }
4061+ else {
4062+ Carp::confess("Couldn't get the version from the dbh while "
4063+ . "creating a VersionParser object: $@");
4064+ }
4065+ $args{innodb_version} = eval { $self->_innodb_version($dbh) };
4066+ }
4067+ elsif ( !ref($_[0]) ) {
4068+ @args{@methods} = $self->_split_version($_[0]);
4069+ }
4070+
4071+ for my $method (@methods) {
4072+ delete $args{$method} unless defined $args{$method};
4073+ }
4074+ @_ = %args if %args;
4075+ }
4076+
4077+ return $self->SUPER::BUILDARGS(@_);
4078+}
4079+
4080+sub _innodb_version {
4081+ my ( $self, $dbh ) = @_;
4082+ return unless $dbh;
4083+ my $innodb_version = "NO";
4084+
4085+ my ($innodb) =
4086+ grep { $_->{engine} =~ m/InnoDB/i }
4087+ map {
4088+ my %hash;
4089+ @hash{ map { lc $_ } keys %$_ } = values %$_;
4090+ \%hash;
4091+ }
4092+ @{ $dbh->selectall_arrayref("SHOW ENGINES", {Slice=>{}}) };
4093+ if ( $innodb ) {
4094+ PTDEBUG && _d("InnoDB support:", $innodb->{support});
4095+ if ( $innodb->{support} =~ m/YES|DEFAULT/i ) {
4096+ my $vars = $dbh->selectrow_hashref(
4097+ "SHOW VARIABLES LIKE 'innodb_version'");
4098+ $innodb_version = !$vars ? "BUILTIN"
4099+ : ($vars->{Value} || $vars->{value});
4100+ }
4101+ else {
4102+ $innodb_version = $innodb->{support}; # probably DISABLED or NO
4103+ }
4104+ }
4105+
4106+ PTDEBUG && _d("InnoDB version:", $innodb_version);
4107+ return $innodb_version;
4108+}
4109+
4110+sub _d {
4111+ my ($package, undef, $line) = caller 0;
4112+ @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
4113+ map { defined $_ ? $_ : 'undef' }
4114+ @_;
4115+ print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
4116+}
4117+
4118+no Lmo;
4119+1;
4120+}
4121+# ###########################################################################
4122+# End VersionParser package
4123+# ###########################################################################
4124+
4125+# ###########################################################################
4126+# Daemon package
4127+# This package is a copy without comments from the original. The original
4128+# with comments and its test file can be found in the Bazaar repository at,
4129+# lib/Daemon.pm
4130+# t/lib/Daemon.t
4131+# See https://launchpad.net/percona-toolkit for more information.
4132+# ###########################################################################
4133+{
4134+package Daemon;
4135+
4136+use strict;
4137+use warnings FATAL => 'all';
4138+use English qw(-no_match_vars);
4139+
4140+use constant PTDEBUG => $ENV{PTDEBUG} || 0;
4141+
4142+use POSIX qw(setsid);
4143+use Fcntl qw(:DEFAULT);
4144+
4145+sub new {
4146+ my ($class, %args) = @_;
4147+ my $self = {
4148+ log_file => $args{log_file},
4149+ pid_file => $args{pid_file},
4150+ daemonize => $args{daemonize},
4151+ force_log_file => $args{force_log_file},
4152+ parent_exit => $args{parent_exit},
4153+ pid_file_owner => 0,
4154+ };
4155+ return bless $self, $class;
4156+}
4157+
4158+sub run {
4159+ my ($self) = @_;
4160+
4161+ my $daemonize = $self->{daemonize};
4162+ my $pid_file = $self->{pid_file};
4163+ my $log_file = $self->{log_file};
4164+ my $force_log_file = $self->{force_log_file};
4165+ my $parent_exit = $self->{parent_exit};
4166+
4167+ PTDEBUG && _d('Starting daemon');
4168+
4169+ if ( $pid_file ) {
4170+ eval {
4171+ $self->_make_pid_file(
4172+ pid => $PID, # parent's pid
4173+ pid_file => $pid_file,
4174+ );
4175+ };
4176+ die "$EVAL_ERROR\n" if $EVAL_ERROR;
4177+ if ( !$daemonize ) {
4178+ $self->{pid_file_owner} = $PID; # parent's pid
4179+ }
4180+ }
4181+
4182+ if ( $daemonize ) {
4183+ defined (my $child_pid = fork()) or die "Cannot fork: $OS_ERROR";
4184+ if ( $child_pid ) {
4185+ PTDEBUG && _d('Forked child', $child_pid);
4186+ $parent_exit->($child_pid) if $parent_exit;
4187+ exit 0;
4188+ }
4189+
4190+ POSIX::setsid() or die "Cannot start a new session: $OS_ERROR";
4191+ chdir '/' or die "Cannot chdir to /: $OS_ERROR";
4192+
4193+ if ( $pid_file ) {
4194+ $self->_update_pid_file(
4195+ pid => $PID, # child's pid
4196+ pid_file => $pid_file,
4197+ );
4198+ $self->{pid_file_owner} = $PID;
4199+ }
4200+ }
4201+
4202+ if ( $daemonize || $force_log_file ) {
4203+ PTDEBUG && _d('Redirecting STDIN to /dev/null');
4204+ close STDIN;
4205+ open STDIN, '/dev/null'
4206+ or die "Cannot reopen STDIN to /dev/null: $OS_ERROR";
4207+ if ( $log_file ) {
4208+ PTDEBUG && _d('Redirecting STDOUT and STDERR to', $log_file);
4209+ close STDOUT;
4210+ open STDOUT, '>>', $log_file
4211+ or die "Cannot open log file $log_file: $OS_ERROR";
4212+
4213+ close STDERR;
4214+ open STDERR, ">&STDOUT"
4215+ or die "Cannot dupe STDERR to STDOUT: $OS_ERROR";
4216+ }
4217+ else {
4218+ if ( -t STDOUT ) {
4219+ PTDEBUG && _d('No log file and STDOUT is a terminal;',
4220+ 'redirecting to /dev/null');
4221+ close STDOUT;
4222+ open STDOUT, '>', '/dev/null'
4223+ or die "Cannot reopen STDOUT to /dev/null: $OS_ERROR";
4224+ }
4225+ if ( -t STDERR ) {
4226+ PTDEBUG && _d('No log file and STDERR is a terminal;',
4227+ 'redirecting to /dev/null');
4228+ close STDERR;
4229+ open STDERR, '>', '/dev/null'
4230+ or die "Cannot reopen STDERR to /dev/null: $OS_ERROR";
4231+ }
4232+ }
4233+
4234+ $OUTPUT_AUTOFLUSH = 1;
4235+ }
4236+
4237+ PTDEBUG && _d('Daemon running');
4238+ return;
4239+}
4240+
4241+sub _make_pid_file {
4242+ my ($self, %args) = @_;
4243+ my @required_args = qw(pid pid_file);
4244+ foreach my $arg ( @required_args ) {
4245+ die "I need a $arg argument" unless $args{$arg};
4246+ };
4247+ my $pid = $args{pid};
4248+ my $pid_file = $args{pid_file};
4249+
4250+ eval {
4251+ sysopen(PID_FH, $pid_file, O_RDWR|O_CREAT|O_EXCL) or die $OS_ERROR;
4252+ print PID_FH $PID, "\n";
4253+ close PID_FH;
4254+ };
4255+ if ( my $e = $EVAL_ERROR ) {
4256+ if ( $e =~ m/file exists/i ) {
4257+ my $old_pid = $self->_check_pid_file(
4258+ pid_file => $pid_file,
4259+ pid => $PID,
4260+ );
4261+ if ( $old_pid ) {
4262+ warn "Overwriting PID file $pid_file because PID $old_pid "
4263+ . "is not running.\n";
4264+ }
4265+ $self->_update_pid_file(
4266+ pid => $PID,
4267+ pid_file => $pid_file
4268+ );
4269+ }
4270+ else {
4271+ die "Error creating PID file $pid_file: $e\n";
4272+ }
4273+ }
4274+
4275+ return;
4276+}
4277+
4278+sub _check_pid_file {
4279+ my ($self, %args) = @_;
4280+ my @required_args = qw(pid_file pid);
4281+ foreach my $arg ( @required_args ) {
4282+ die "I need a $arg argument" unless $args{$arg};
4283+ };
4284+ my $pid_file = $args{pid_file};
4285+ my $pid = $args{pid};
4286+
4287+ PTDEBUG && _d('Checking if PID in', $pid_file, 'is running');
4288+
4289+ if ( ! -f $pid_file ) {
4290+ PTDEBUG && _d('PID file', $pid_file, 'does not exist');
4291+ return;
4292+ }
4293+
4294+ open my $fh, '<', $pid_file
4295+ or die "Error opening $pid_file: $OS_ERROR";
4296+ my $existing_pid = do { local $/; <$fh> };
4297+ chomp($existing_pid) if $existing_pid;
4298+ close $fh
4299+ or die "Error closing $pid_file: $OS_ERROR";
4300+
4301+ if ( $existing_pid ) {
4302+ if ( $existing_pid == $pid ) {
4303+ warn "The current PID $pid already holds the PID file $pid_file\n";
4304+ return;
4305+ }
4306+ else {
4307+ PTDEBUG && _d('Checking if PID', $existing_pid, 'is running');
4308+ my $pid_is_alive = kill 0, $existing_pid;
4309+ if ( $pid_is_alive ) {
4310+ die "PID file $pid_file exists and PID $existing_pid is running\n";
4311+ }
4312+ }
4313+ }
4314+ else {
4315+ die "PID file $pid_file exists but it is empty. Remove the file "
4316+ . "if the process is no longer running.\n";
4317+ }
4318+
4319+ return $existing_pid;
4320+}
4321+
4322+sub _update_pid_file {
4323+ my ($self, %args) = @_;
4324+ my @required_args = qw(pid pid_file);
4325+ foreach my $arg ( @required_args ) {
4326+ die "I need a $arg argument" unless $args{$arg};
4327+ };
4328+ my $pid = $args{pid};
4329+ my $pid_file = $args{pid_file};
4330+
4331+ open my $fh, '>', $pid_file
4332+ or die "Cannot open $pid_file: $OS_ERROR";
4333+ print { $fh } $pid, "\n"
4334+ or die "Cannot print to $pid_file: $OS_ERROR";
4335+ close $fh
4336+ or warn "Cannot close $pid_file: $OS_ERROR";
4337+
4338+ return;
4339+}
4340+
4341+sub remove_pid_file {
4342+ my ($self, $pid_file) = @_;
4343+ $pid_file ||= $self->{pid_file};
4344+ if ( $pid_file && -f $pid_file ) {
4345+ unlink $self->{pid_file}
4346+ or warn "Cannot remove PID file $pid_file: $OS_ERROR";
4347+ PTDEBUG && _d('Removed PID file');
4348+ }
4349+ else {
4350+ PTDEBUG && _d('No PID to remove');
4351+ }
4352+ return;
4353+}
4354+
4355+sub DESTROY {
4356+ my ($self) = @_;
4357+
4358+ if ( $self->{pid_file_owner} == $PID ) {
4359+ $self->remove_pid_file();
4360+ }
4361+
4362+ return;
4363+}
4364+
4365+sub _d {
4366+ my ($package, undef, $line) = caller 0;
4367+ @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
4368+ map { defined $_ ? $_ : 'undef' }
4369+ @_;
4370+ print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
4371+}
4372+
4373+1;
4374+}
4375+# ###########################################################################
4376+# End Daemon package
4377+# ###########################################################################
4378+
4379+# ###########################################################################
4380+# Transformers package
4381+# This package is a copy without comments from the original. The original
4382+# with comments and its test file can be found in the Bazaar repository at,
4383+# lib/Transformers.pm
4384+# t/lib/Transformers.t
4385+# See https://launchpad.net/percona-toolkit for more information.
4386+# ###########################################################################
4387+{
4388+package Transformers;
4389+
4390+use strict;
4391+use warnings FATAL => 'all';
4392+use English qw(-no_match_vars);
4393+use constant PTDEBUG => $ENV{PTDEBUG} || 0;
4394+
4395+use Time::Local qw(timegm timelocal);
4396+use Digest::MD5 qw(md5_hex);
4397+use B qw();
4398+
4399+BEGIN {
4400+ require Exporter;
4401+ our @ISA = qw(Exporter);
4402+ our %EXPORT_TAGS = ();
4403+ our @EXPORT = ();
4404+ our @EXPORT_OK = qw(
4405+ micro_t
4406+ percentage_of
4407+ secs_to_time
4408+ time_to_secs
4409+ shorten
4410+ ts
4411+ parse_timestamp
4412+ unix_timestamp
4413+ any_unix_timestamp
4414+ make_checksum
4415+ crc32
4416+ encode_json
4417+ );
4418+}
4419+
4420+our $mysql_ts = qr/(\d\d)(\d\d)(\d\d) +(\d+):(\d+):(\d+)(\.\d+)?/;
4421+our $proper_ts = qr/(\d\d\d\d)-(\d\d)-(\d\d)[T ](\d\d):(\d\d):(\d\d)(\.\d+)?/;
4422+our $n_ts = qr/(\d{1,5})([shmd]?)/; # Limit \d{1,5} because \d{6} looks
4423+
4424+sub micro_t {
4425+ my ( $t, %args ) = @_;
4426+ my $p_ms = defined $args{p_ms} ? $args{p_ms} : 0; # precision for ms vals
4427+ my $p_s = defined $args{p_s} ? $args{p_s} : 0; # precision for s vals
4428+ my $f;
4429+
4430+ $t = 0 if $t < 0;
4431+
4432+ $t = sprintf('%.17f', $t) if $t =~ /e/;
4433+
4434+ $t =~ s/\.(\d{1,6})\d*/\.$1/;
4435+
4436+ if ($t > 0 && $t <= 0.000999) {
4437+ $f = ($t * 1000000) . 'us';
4438+ }
4439+ elsif ($t >= 0.001000 && $t <= 0.999999) {
4440+ $f = sprintf("%.${p_ms}f", $t * 1000);
4441+ $f = ($f * 1) . 'ms'; # * 1 to remove insignificant zeros
4442+ }
4443+ elsif ($t >= 1) {
4444+ $f = sprintf("%.${p_s}f", $t);
4445+ $f = ($f * 1) . 's'; # * 1 to remove insignificant zeros
4446+ }
4447+ else {
4448+ $f = 0; # $t should = 0 at this point
4449+ }
4450+
4451+ return $f;
4452+}
4453+
4454+sub percentage_of {
4455+ my ( $is, $of, %args ) = @_;
4456+ my $p = $args{p} || 0; # float precision
4457+ my $fmt = $p ? "%.${p}f" : "%d";
4458+ return sprintf $fmt, ($is * 100) / ($of ||= 1);
4459+}
4460+
4461+sub secs_to_time {
4462+ my ( $secs, $fmt ) = @_;
4463+ $secs ||= 0;
4464+ return '00:00' unless $secs;
4465+
4466+ $fmt ||= $secs >= 86_400 ? 'd'
4467+ : $secs >= 3_600 ? 'h'
4468+ : 'm';
4469+
4470+ return
4471+ $fmt eq 'd' ? sprintf(
4472+ "%d+%02d:%02d:%02d",
4473+ int($secs / 86_400),
4474+ int(($secs % 86_400) / 3_600),
4475+ int(($secs % 3_600) / 60),
4476+ $secs % 60)
4477+ : $fmt eq 'h' ? sprintf(
4478+ "%02d:%02d:%02d",
4479+ int(($secs % 86_400) / 3_600),
4480+ int(($secs % 3_600) / 60),
4481+ $secs % 60)
4482+ : sprintf(
4483+ "%02d:%02d",
4484+ int(($secs % 3_600) / 60),
4485+ $secs % 60);
4486+}
4487+
4488+sub time_to_secs {
4489+ my ( $val, $default_suffix ) = @_;
4490+ die "I need a val argument" unless defined $val;
4491+ my $t = 0;
4492+ my ( $prefix, $num, $suffix ) = $val =~ m/([+-]?)(\d+)([a-z])?$/;
4493+ $suffix = $suffix || $default_suffix || 's';
4494+ if ( $suffix =~ m/[smhd]/ ) {
4495+ $t = $suffix eq 's' ? $num * 1 # Seconds
4496+ : $suffix eq 'm' ? $num * 60 # Minutes
4497+ : $suffix eq 'h' ? $num * 3600 # Hours
4498+ : $num * 86400; # Days
4499+
4500+ $t *= -1 if $prefix && $prefix eq '-';
4501+ }
4502+ else {
4503+ die "Invalid suffix for $val: $suffix";
4504+ }
4505+ return $t;
4506+}
4507+
4508+sub shorten {
4509+ my ( $num, %args ) = @_;
4510+ my $p = defined $args{p} ? $args{p} : 2; # float precision
4511+ my $d = defined $args{d} ? $args{d} : 1_024; # divisor
4512+ my $n = 0;
4513+ my @units = ('', qw(k M G T P E Z Y));
4514+ while ( $num >= $d && $n < @units - 1 ) {
4515+ $num /= $d;
4516+ ++$n;
4517+ }
4518+ return sprintf(
4519+ $num =~ m/\./ || $n
4520+ ? "%.${p}f%s"
4521+ : '%d',
4522+ $num, $units[$n]);
4523+}
4524+
4525+sub ts {
4526+ my ( $time, $gmt ) = @_;
4527+ my ( $sec, $min, $hour, $mday, $mon, $year )
4528+ = $gmt ? gmtime($time) : localtime($time);
4529+ $mon += 1;
4530+ $year += 1900;
4531+ my $val = sprintf("%d-%02d-%02dT%02d:%02d:%02d",
4532+ $year, $mon, $mday, $hour, $min, $sec);
4533+ if ( my ($us) = $time =~ m/(\.\d+)$/ ) {
4534+ $us = sprintf("%.6f", $us);
4535+ $us =~ s/^0\././;
4536+ $val .= $us;
4537+ }
4538+ return $val;
4539+}
4540+
4541+sub parse_timestamp {
4542+ my ( $val ) = @_;
4543+ if ( my($y, $m, $d, $h, $i, $s, $f)
4544+ = $val =~ m/^$mysql_ts$/ )
4545+ {
4546+ return sprintf "%d-%02d-%02d %02d:%02d:"
4547+ . (defined $f ? '%09.6f' : '%02d'),
4548+ $y + 2000, $m, $d, $h, $i, (defined $f ? $s + $f : $s);
4549+ }
4550+ elsif ( $val =~ m/^$proper_ts$/ ) {
4551+ return $val;
4552+ }
4553+ return $val;
4554+}
4555+
4556+sub unix_timestamp {
4557+ my ( $val, $gmt ) = @_;
4558+ if ( my($y, $m, $d, $h, $i, $s, $us) = $val =~ m/^$proper_ts$/ ) {
4559+ $val = $gmt
4560+ ? timegm($s, $i, $h, $d, $m - 1, $y)
4561+ : timelocal($s, $i, $h, $d, $m - 1, $y);
4562+ if ( defined $us ) {
4563+ $us = sprintf('%.6f', $us);
4564+ $us =~ s/^0\././;
4565+ $val .= $us;
4566+ }
4567+ }
4568+ return $val;
4569+}
4570+
4571+sub any_unix_timestamp {
4572+ my ( $val, $callback ) = @_;
4573+
4574+ if ( my ($n, $suffix) = $val =~ m/^$n_ts$/ ) {
4575+ $n = $suffix eq 's' ? $n # Seconds
4576+ : $suffix eq 'm' ? $n * 60 # Minutes
4577+ : $suffix eq 'h' ? $n * 3600 # Hours
4578+ : $suffix eq 'd' ? $n * 86400 # Days
4579+ : $n; # default: Seconds
4580+ PTDEBUG && _d('ts is now - N[shmd]:', $n);
4581+ return time - $n;
4582+ }
4583+ elsif ( $val =~ m/^\d{9,}/ ) {
4584+ PTDEBUG && _d('ts is already a unix timestamp');
4585+ return $val;
4586+ }
4587+ elsif ( my ($ymd, $hms) = $val =~ m/^(\d{6})(?:\s+(\d+:\d+:\d+))?/ ) {
4588+ PTDEBUG && _d('ts is MySQL slow log timestamp');
4589+ $val .= ' 00:00:00' unless $hms;
4590+ return unix_timestamp(parse_timestamp($val));
4591+ }
4592+ elsif ( ($ymd, $hms) = $val =~ m/^(\d{4}-\d\d-\d\d)(?:[T ](\d+:\d+:\d+))?/) {
4593+ PTDEBUG && _d('ts is properly formatted timestamp');
4594+ $val .= ' 00:00:00' unless $hms;
4595+ return unix_timestamp($val);
4596+ }
4597+ else {
4598+ PTDEBUG && _d('ts is MySQL expression');
4599+ return $callback->($val) if $callback && ref $callback eq 'CODE';
4600+ }
4601+
4602+ PTDEBUG && _d('Unknown ts type:', $val);
4603+ return;
4604+}
4605+
4606+sub make_checksum {
4607+ my ( $val ) = @_;
4608+ my $checksum = uc substr(md5_hex($val), -16);
4609+ PTDEBUG && _d($checksum, 'checksum for', $val);
4610+ return $checksum;
4611+}
4612+
4613+sub crc32 {
4614+ my ( $string ) = @_;
4615+ return unless $string;
4616+ my $poly = 0xEDB88320;
4617+ my $crc = 0xFFFFFFFF;
4618+ foreach my $char ( split(//, $string) ) {
4619+ my $comp = ($crc ^ ord($char)) & 0xFF;
4620+ for ( 1 .. 8 ) {
4621+ $comp = $comp & 1 ? $poly ^ ($comp >> 1) : $comp >> 1;
4622+ }
4623+ $crc = (($crc >> 8) & 0x00FFFFFF) ^ $comp;
4624+ }
4625+ return $crc ^ 0xFFFFFFFF;
4626+}
4627+
4628+my $got_json = eval { require JSON };
4629+sub encode_json {
4630+ return JSON::encode_json(@_) if $got_json;
4631+ my ( $data ) = @_;
4632+ return (object_to_json($data) || '');
4633+}
4634+
4635+
4636+sub object_to_json {
4637+ my ($obj) = @_;
4638+ my $type = ref($obj);
4639+
4640+ if($type eq 'HASH'){
4641+ return hash_to_json($obj);
4642+ }
4643+ elsif($type eq 'ARRAY'){
4644+ return array_to_json($obj);
4645+ }
4646+ else {
4647+ return value_to_json($obj);
4648+ }
4649+}
4650+
4651+sub hash_to_json {
4652+ my ($obj) = @_;
4653+ my @res;
4654+ for my $k ( sort { $a cmp $b } keys %$obj ) {
4655+ push @res, string_to_json( $k )
4656+ . ":"
4657+ . ( object_to_json( $obj->{$k} ) || value_to_json( $obj->{$k} ) );
4658+ }
4659+ return '{' . ( @res ? join( ",", @res ) : '' ) . '}';
4660+}
4661+
4662+sub array_to_json {
4663+ my ($obj) = @_;
4664+ my @res;
4665+
4666+ for my $v (@$obj) {
4667+ push @res, object_to_json($v) || value_to_json($v);
4668+ }
4669+
4670+ return '[' . ( @res ? join( ",", @res ) : '' ) . ']';
4671+}
4672+
4673+sub value_to_json {
4674+ my ($value) = @_;
4675+
4676+ return 'null' if(!defined $value);
4677+
4678+ my $b_obj = B::svref_2object(\$value); # for round trip problem
4679+ my $flags = $b_obj->FLAGS;
4680+ return $value # as is
4681+ if $flags & ( B::SVp_IOK | B::SVp_NOK ) and !( $flags & B::SVp_POK ); # SvTYPE is IV or NV?
4682+
4683+ my $type = ref($value);
4684+
4685+ if( !$type ) {
4686+ return string_to_json($value);
4687+ }
4688+ else {
4689+ return 'null';
4690+ }
4691+
4692+}
4693+
4694+my %esc = (
4695+ "\n" => '\n',
4696+ "\r" => '\r',
4697+ "\t" => '\t',
4698+ "\f" => '\f',
4699+ "\b" => '\b',
4700+ "\"" => '\"',
4701+ "\\" => '\\\\',
4702+ "\'" => '\\\'',
4703+);
4704+
4705+sub string_to_json {
4706+ my ($arg) = @_;
4707+
4708+ $arg =~ s/([\x22\x5c\n\r\t\f\b])/$esc{$1}/g;
4709+ $arg =~ s/\//\\\//g;
4710+ $arg =~ s/([\x00-\x08\x0b\x0e-\x1f])/'\\u00' . unpack('H2', $1)/eg;
4711+
4712+ utf8::upgrade($arg);
4713+ utf8::encode($arg);
4714+
4715+ return '"' . $arg . '"';
4716+}
4717+
4718+sub _d {
4719+ my ($package, undef, $line) = caller 0;
4720+ @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
4721+ map { defined $_ ? $_ : 'undef' }
4722+ @_;
4723+ print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
4724+}
4725+
4726+1;
4727+}
4728+# ###########################################################################
4729+# End Transformers package
4730+# ###########################################################################
4731+
4732+# ###########################################################################
4733+# Safeguards package
4734+# This package is a copy without comments from the original. The original
4735+# with comments and its test file can be found in the Bazaar repository at,
4736+# lib/Safeguards.pm
4737+# t/lib/Safeguards.t
4738+# See https://launchpad.net/percona-toolkit for more information.
4739+# ###########################################################################
4740+{
4741+package Safeguards;
4742+
4743+use strict;
4744+use warnings FATAL => 'all';
4745+use English qw(-no_match_vars);
4746+
4747+use constant PTDEBUG => $ENV{PTDEBUG} || 0;
4748+
4749+sub new {
4750+ my ($class, %args) = @_;
4751+ my $self = {
4752+ disk_bytes_free => $args{disk_bytes_free} || 104857600, # 100 MiB
4753+ disk_pct_free => $args{disk_pct_free} || 5,
4754+ };
4755+ return bless $self, $class;
4756+}
4757+
4758+sub get_disk_space {
4759+ my ($self, %args) = @_;
4760+ my $filesystem = $args{filesystem} || $ENV{PWD};
4761+
4762+ my $disk_space = `df -P -k "$filesystem"`;
4763+ chop($disk_space) if $disk_space;
4764+ PTDEBUG && _d('Disk space on', $filesystem, $disk_space);
4765+
4766+ return $disk_space;
4767+}
4768+
4769+sub check_disk_space() {
4770+ my ($self, %args) = @_;
4771+ my $disk_space = $args{disk_space};
4772+ PTDEBUG && _d("Checking disk space:\n", $disk_space);
4773+
4774+ my ($partition) = $disk_space =~ m/^\s*(\/.+)/m;
4775+ PTDEBUG && _d('Partition:', $partition);
4776+ die "Failed to parse partition from disk space:\n$disk_space"
4777+ unless $partition;
4778+
4779+ my (undef, undef, $bytes_used, $bytes_free, $pct_used, undef)
4780+ = $partition =~ m/(\S+)/g;
4781+ PTDEBUG && _d('Bytes used:', $bytes_used, 'free:', $bytes_free,
4782+ 'Percentage used:', $pct_used);
4783+
4784+ $bytes_used = ($bytes_used || 0) * 1024;
4785+ $bytes_free = ($bytes_free || 0) * 1024;
4786+
4787+ $pct_used =~ s/%//;
4788+ my $pct_free = 100 - ($pct_used || 0);
4789+
4790+ return $bytes_free >= $self->{disk_bytes_free}
4791+ && $pct_free >= $self->{disk_pct_free};
4792+}
4793+
4794+sub _d {
4795+ my ($package, undef, $line) = caller 0;
4796+ @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
4797+ map { defined $_ ? $_ : 'undef' }
4798+ @_;
4799+ print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
4800+}
4801+
4802+1;
4803+}
4804+# ###########################################################################
4805+# End Safeguards package
4806+# ###########################################################################
4807+
4808+# ###########################################################################
4809+# Percona::Agent::Logger package
4810+# This package is a copy without comments from the original. The original
4811+# with comments and its test file can be found in the Bazaar repository at,
4812+# lib/Percona/Agent/Logger.pm
4813+# t/lib/Percona/Agent/Logger.t
4814+# See https://launchpad.net/percona-toolkit for more information.
4815+# ###########################################################################
4816+{
4817+package Percona::Agent::Logger;
4818+
4819+use strict;
4820+use warnings FATAL => 'all';
4821+use English qw(-no_match_vars);
4822+
4823+use constant PTDEBUG => $ENV{PTDEBUG} || 0;
4824+
4825+use POSIX qw(SIGALRM);
4826+
4827+use Lmo;
4828+use Transformers;
4829+use Percona::WebAPI::Resource::LogEntry;
4830+
4831+Transformers->import(qw(ts));
4832+
4833+has 'exit_status' => (
4834+ is => 'rw',
4835+ isa => 'ScalarRef',
4836+ required => 1,
4837+);
4838+
4839+has 'pid' => (
4840+ is => 'ro',
4841+ isa => 'Int',
4842+ required => 1,
4843+);
4844+
4845+has 'service' => (
4846+ is => 'rw',
4847+ isa => 'Maybe[Str]',
4848+ required => 0,
4849+ default => sub { return; },
4850+);
4851+
4852+has 'data_ts' => (
4853+ is => 'rw',
4854+ isa => 'Maybe[Int]',
4855+ required => 0,
4856+ default => sub { return; },
4857+);
4858+
4859+has 'online_logging' => (
4860+ is => 'ro',
4861+ isa => 'Bool',
4862+ required => 0,
4863+ default => sub { return 1 },
4864+);
4865+
4866+has 'online_logging_enabled' => (
4867+ is => 'rw',
4868+ isa => 'Bool',
4869+ required => 0,
4870+ default => sub { return 0 },
4871+);
4872+
4873+has 'quiet' => (
4874+ is => 'rw',
4875+ isa => 'Int',
4876+ required => 0,
4877+ default => sub { return 0 },
4878+);
4879+
4880+has '_buffer' => (
4881+ is => 'rw',
4882+ isa => 'ArrayRef',
4883+ required => 0,
4884+ default => sub { return []; },
4885+);
4886+
4887+has '_pipe_write' => (
4888+ is => 'rw',
4889+ isa => 'Maybe[FileHandle]',
4890+ required => 0,
4891+);
4892+
4893+sub read_stdin {
4894+ my ( $t ) = @_;
4895+
4896+ POSIX::sigaction(
4897+ SIGALRM,
4898+ POSIX::SigAction->new(sub { die 'read timeout'; }),
4899+ ) or die "Error setting SIGALRM handler: $OS_ERROR";
4900+
4901+ my $timeout = 0;
4902+ my @lines;
4903+ eval {
4904+ alarm $t;
4905+ while(defined(my $line = <STDIN>)) {
4906+ push @lines, $line;
4907+ }
4908+ alarm 0;
4909+ };
4910+ if ( $EVAL_ERROR ) {
4911+ PTDEBUG && _d('Read error:', $EVAL_ERROR);
4912+ die $EVAL_ERROR unless $EVAL_ERROR =~ m/read timeout/;
4913+ $timeout = 1;
4914+ }
4915+ return unless scalar @lines || $timeout;
4916+ return \@lines;
4917+}
4918+
4919+sub start_online_logging {
4920+ my ($self, %args) = @_;
4921+ my $client = $args{client};
4922+ my $log_link = $args{log_link};
4923+ my $read_timeout = $args{read_timeout} || 3;
4924+
4925+ return unless $self->online_logging;
4926+
4927+ $self->info("Starting online logging. No more log entries will be printed here. "
4928+ . "Agent logs are accessible through the web interface.");
4929+
4930+ my $pid = open(my $pipe_write, "|-");
4931+
4932+ if ($pid) {
4933+ select $pipe_write;
4934+ $OUTPUT_AUTOFLUSH = 1;
4935+ $self->_pipe_write($pipe_write);
4936+ $self->online_logging_enabled(1);
4937+ }
4938+ else {
4939+ my @log_entries;
4940+ my $n_errors = 0;
4941+ my $oktorun = 1;
4942+ QUEUE:
4943+ while ($oktorun) {
4944+ my $lines = read_stdin($read_timeout);
4945+ last QUEUE unless $lines;
4946+ LINE:
4947+ while ( defined(my $line = shift @$lines) ) {
4948+ my ($ts, $level, $n_lines, $msg) = $line =~ m/^([^,]+),([^,]+),([^,]+),(.+)/s;
4949+ if ( !$ts || !$level || !$n_lines || !$msg ) {
4950+ warn "$line\n";
4951+ next LINE;
4952+ }
4953+ if ( $n_lines > 1 ) {
4954+ $n_lines--; # first line
4955+ for ( 1..$n_lines ) {
4956+ $msg .= shift @$lines;
4957+ }
4958+ }
4959+
4960+ push @log_entries, Percona::WebAPI::Resource::LogEntry->new(
4961+ pid => $self->pid,
4962+ entry_ts => $ts,
4963+ log_level => $level,
4964+ message => $msg,
4965+ ($self->service ? (service => $self->service) : ()),
4966+ ($self->data_ts ? (data_ts => $self->data_ts) : ()),
4967+ );
4968+ } # LINE
4969+
4970+ if ( scalar @log_entries ) {
4971+ eval {
4972+ $client->post(
4973+ link => $log_link,
4974+ resources => \@log_entries,
4975+ );
4976+ };
4977+ if ( my $e = $EVAL_ERROR ) {
4978+ if ( ++$n_errors <= 10 ) {
4979+ warn "Error sending log entry to API: $e";
4980+ if ( $n_errors == 10 ) {
4981+ my $ts = ts(time, 1); # 1=UTC
4982+ warn "$ts WARNING $n_errors consecutive errors, no more "
4983+ . "error messages will be printed until log entries "
4984+ . "are sent successfully again.\n";
4985+ }
4986+ }
4987+ }
4988+ else {
4989+ @log_entries = ();
4990+ $n_errors = 0;
4991+ }
4992+ } # have log entries
4993+
4994+ my $n_log_entries = scalar @log_entries;
4995+ if ( $n_log_entries > 1_000 ) {
4996+ warn "$n_log_entries log entries in send buffer, "
4997+ . "removing first 100 to avoid excessive usage.\n";
4998+ @log_entries = @log_entries[100..($n_log_entries-1)];
4999+ }
5000+ } # QUEUE
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to all changes: