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

Proposed by Daniel Nichter
Status: Merged
Approved by: Daniel Nichter
Approved revision: 587
Merged at revision: 583
Proposed branch: lp:~percona-toolkit-dev/percona-toolkit/release-2.2.3
Merge into: lp:percona-toolkit/2.2
Diff against target: 31484 lines (+26570/-2236)
103 files modified
Changelog (+5/-0)
MANIFEST (+1/-0)
Makefile.PL (+1/-1)
bin/pt-agent (+9386/-0)
bin/pt-align (+1/-1)
bin/pt-archiver (+47/-2)
bin/pt-config-diff (+47/-2)
bin/pt-deadlock-logger (+47/-2)
bin/pt-diskstats (+47/-2)
bin/pt-duplicate-key-checker (+47/-2)
bin/pt-fifo-split (+1/-1)
bin/pt-find (+47/-2)
bin/pt-fingerprint (+1/-1)
bin/pt-fk-error-logger (+47/-2)
bin/pt-heartbeat (+48/-3)
bin/pt-index-usage (+47/-2)
bin/pt-ioprofile (+1/-1)
bin/pt-kill (+47/-2)
bin/pt-mext (+1/-1)
bin/pt-mysql-summary (+1/-1)
bin/pt-online-schema-change (+47/-2)
bin/pt-pmp (+1/-1)
bin/pt-query-digest (+411/-73)
bin/pt-show-grants (+1/-1)
bin/pt-sift (+1/-1)
bin/pt-slave-delay (+47/-2)
bin/pt-slave-find (+1/-1)
bin/pt-slave-restart (+47/-2)
bin/pt-stalk (+1/-1)
bin/pt-summary (+1/-1)
bin/pt-table-checksum (+47/-2)
bin/pt-table-sync (+47/-2)
bin/pt-table-usage (+1/-1)
bin/pt-upgrade (+47/-2)
bin/pt-variable-advisor (+47/-2)
bin/pt-visual-explain (+1/-1)
config/deb/changelog (+7/-0)
config/sphinx-build/conf.py (+1/-1)
docs/percona-toolkit.pod (+5/-1)
docs/release_notes.rst (+21/-0)
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 (+46/-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 (+0/-703)
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/output_json_slow002.txt (+229/-130)
t/pt-query-digest/samples/output_json_tcpdump021.txt (+224/-237)
t/pt-query-digest/slowlog_analyses.t (+1/-1)
util/check-dev-env (+1/-0)
To merge this branch: bzr merge lp:~percona-toolkit-dev/percona-toolkit/release-2.2.3
Reviewer Review Type Date Requested Status
Daniel Nichter Approve
Review via email: mp+170444@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Daniel Nichter (daniel-nichter) :
review: Approve

Preview Diff

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

Subscribers

People subscribed via source and target branches