diff --git a/bin/pt-agent b/bin/pt-agent new file mode 100755 index 00000000..2367df63 --- /dev/null +++ b/bin/pt-agent @@ -0,0 +1,4847 @@ +#!/usr/bin/env perl + +# This program is part of Percona Toolkit: http://www.percona.com/software/ +# See "COPYRIGHT, LICENSE, AND WARRANTY" at the end of this file for legal +# notices and disclaimers. + +use strict; +use warnings FATAL => 'all'; + +# This tool is "fat-packed": most of its dependent modules are embedded +# in this file. Setting %INC to this file for each module makes Perl aware +# of this so it will not try to load the module from @INC. See the tool's +# documentation for a full list of dependencies. +BEGIN { + $INC{$_} = __FILE__ for map { (my $pkg = "$_.pm") =~ s!::!/!g; $pkg } (qw( + Percona::Toolkit + VersionCheck + HTTPMicro + Pingback + DSNParser + OptionParser + Mo + Cxn + Percona::XtraDB::Cluster + Quoter + VersionParser + TableParser + TableNibbler + MasterSlave + RowChecksum + NibbleIterator + OobNibbleIterator + Daemon + SchemaIterator + Retry + Transformers + Progress + ReplicaLagWaiter + MySQLStatusWaiter + WeightedAvgRate + IndexLength + Runtime + )); +} + +# ########################################################################### +# Percona::Toolkit package +# This package is a copy without comments from the original. The original +# with comments and its test file can be found in the Bazaar repository at, +# lib/Percona/Toolkit.pm +# t/lib/Percona/Toolkit.t +# See https://launchpad.net/percona-toolkit for more information. +# ########################################################################### +{ +package Percona::Toolkit; +our $VERSION = '2.1.7'; + +1; +} +# ########################################################################### +# End Percona::Toolkit package +# ########################################################################### + +# ########################################################################### +# VersionCheck package +# This package is a copy without comments from the original. The original +# with comments and its test file can be found in the Bazaar repository at, +# lib/VersionCheck.pm +# t/lib/VersionCheck.t +# See https://launchpad.net/percona-toolkit for more information. +# ########################################################################### +{ +package VersionCheck; + +use strict; +use warnings FATAL => 'all'; +use English qw(-no_match_vars); + +use constant PTDEBUG => $ENV{PTDEBUG} || 0; + +use File::Basename (); +use Data::Dumper (); + +sub Dumper { + local $Data::Dumper::Indent = 1; + local $Data::Dumper::Sortkeys = 1; + local $Data::Dumper::Quotekeys = 0; + + Data::Dumper::Dumper(@_); +} + +sub new { + my ($class, %args) = @_; + my $self = { + valid_types => qr/ + ^(?: + os_version + |perl_version + |perl_module_version + |mysql_variable + |bin_version + )$/x, + }; + return bless $self, $class; +} + +sub parse_server_response { + my ($self, %args) = @_; + my @required_args = qw(response); + foreach my $arg ( @required_args ) { + die "I need a $arg arugment" unless $args{$arg}; + } + my ($response) = @args{@required_args}; + + my %items = map { + my ($item, $type, $vars) = split(";", $_); + if ( !defined $args{split_vars} || $args{split_vars} ) { + $vars = [ split(",", ($vars || '')) ]; + } + $item => { + item => $item, + type => $type, + vars => $vars, + }; + } split("\n", $response); + + PTDEBUG && _d('Items:', Dumper(\%items)); + + return \%items; +} + +sub get_versions { + my ($self, %args) = @_; + my @required_args = qw(items); + foreach my $arg ( @required_args ) { + die "I need a $arg arugment" unless $args{$arg}; + } + my ($items) = @args{@required_args}; + + my %versions; + foreach my $item ( values %$items ) { + next unless $self->valid_item($item); + + eval { + my $func = 'get_' . $item->{type}; + my $version = $self->$func( + item => $item, + instances => $args{instances}, + ); + if ( $version ) { + chomp $version unless ref($version); + $versions{$item->{item}} = $version; + } + }; + if ( $EVAL_ERROR ) { + PTDEBUG && _d('Error getting version for', Dumper($item), $EVAL_ERROR); + } + } + + return \%versions; +} + +sub valid_item { + my ($self, $item) = @_; + return unless $item; + + if ( ($item->{type} || '') !~ m/$self->{valid_types}/ ) { + PTDEBUG && _d('Invalid type:', $item->{type}); + return; + } + + return 1; +} + +sub get_os_version { + my ($self) = @_; + + if ( $OSNAME eq 'MSWin32' ) { + require Win32; + return Win32::GetOSDisplayName(); + } + + chomp(my $platform = `uname -s`); + PTDEBUG && _d('platform:', $platform); + return $OSNAME unless $platform; + + chomp(my $lsb_release + = `which lsb_release 2>/dev/null | awk '{print \$1}'` || ''); + PTDEBUG && _d('lsb_release:', $lsb_release); + + my $release = ""; + + if ( $platform eq 'Linux' ) { + if ( -f "/etc/fedora-release" ) { + $release = `cat /etc/fedora-release`; + } + elsif ( -f "/etc/redhat-release" ) { + $release = `cat /etc/redhat-release`; + } + elsif ( -f "/etc/system-release" ) { + $release = `cat /etc/system-release`; + } + elsif ( $lsb_release ) { + $release = `$lsb_release -ds`; + } + elsif ( -f "/etc/lsb-release" ) { + $release = `grep DISTRIB_DESCRIPTION /etc/lsb-release`; + $release =~ s/^\w+="([^"]+)".+/$1/; + } + elsif ( -f "/etc/debian_version" ) { + chomp(my $rel = `cat /etc/debian_version`); + $release = "Debian $rel"; + if ( -f "/etc/apt/sources.list" ) { + 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}'`); + $release .= " ($code_name)" if $code_name; + } + } + elsif ( -f "/etc/os-release" ) { # openSUSE + chomp($release = `grep PRETTY_NAME /etc/os-release`); + $release =~ s/^PRETTY_NAME="(.+)"$/$1/; + } + elsif ( `ls /etc/*release 2>/dev/null` ) { + if ( `grep DISTRIB_DESCRIPTION /etc/*release 2>/dev/null` ) { + $release = `grep DISTRIB_DESCRIPTION /etc/*release | head -n1`; + } + else { + $release = `cat /etc/*release | head -n1`; + } + } + } + elsif ( $platform =~ m/(?:BSD|^Darwin)$/ ) { + my $rel = `uname -r`; + $release = "$platform $rel"; + } + elsif ( $platform eq "SunOS" ) { + my $rel = `head -n1 /etc/release` || `uname -r`; + $release = "$platform $rel"; + } + + if ( !$release ) { + PTDEBUG && _d('Failed to get the release, using platform'); + $release = $platform; + } + chomp($release); + + $release =~ s/^"|"$//g; + + PTDEBUG && _d('OS version =', $release); + return $release; +} + +sub get_perl_version { + my ($self, %args) = @_; + my $item = $args{item}; + return unless $item; + + my $version = sprintf '%vd', $PERL_VERSION; + PTDEBUG && _d('Perl version', $version); + return $version; +} + +sub get_perl_module_version { + my ($self, %args) = @_; + my $item = $args{item}; + return unless $item; + + my $var = $item->{item} . '::VERSION'; + my $version = _get_scalar($var); + PTDEBUG && _d('Perl version for', $var, '=', "$version"); + + return $version ? "$version" : $version; +} + +sub _get_scalar { + no strict; + return ${*{shift()}}; +} + +sub get_mysql_variable { + my $self = shift; + return $self->_get_from_mysql( + show => 'VARIABLES', + @_, + ); +} + +sub _get_from_mysql { + my ($self, %args) = @_; + my $show = $args{show}; + my $item = $args{item}; + my $instances = $args{instances}; + return unless $show && $item; + + if ( !$instances || !@$instances ) { + if ( $ENV{PTVCDEBUG} || PTDEBUG ) { + _d('Cannot check', $item, 'because there are no MySQL instances'); + } + return; + } + + my @versions; + my %version_for; + foreach my $instance ( @$instances ) { + my $dbh = $instance->{dbh}; + local $dbh->{FetchHashKeyName} = 'NAME_lc'; + my $sql = qq/SHOW $show/; + PTDEBUG && _d($sql); + my $rows = $dbh->selectall_hashref($sql, 'variable_name'); + + my @versions; + foreach my $var ( @{$item->{vars}} ) { + $var = lc($var); + my $version = $rows->{$var}->{value}; + PTDEBUG && _d('MySQL version for', $item->{item}, '=', $version, + 'on', $instance->{name}); + push @versions, $version; + } + + $version_for{ $instance->{id} } = join(' ', @versions); + } + + return \%version_for; +} + +sub get_bin_version { + my ($self, %args) = @_; + my $item = $args{item}; + my $cmd = $item->{item}; + return unless $cmd; + + my $sanitized_command = File::Basename::basename($cmd); + PTDEBUG && _d('cmd:', $cmd, 'sanitized:', $sanitized_command); + return if $sanitized_command !~ /\A[a-zA-Z0-9_-]+\z/; + + my $output = `$sanitized_command --version 2>&1`; + PTDEBUG && _d('output:', $output); + + my ($version) = $output =~ /v?([0-9]+\.[0-9]+(?:\.[\w-]+)?)/; + + PTDEBUG && _d('Version for', $sanitized_command, '=', $version); + return $version; +} + +sub _d { + my ($package, undef, $line) = caller 0; + @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; } + map { defined $_ ? $_ : 'undef' } + @_; + print STDERR "# $package:$line $PID ", join(' ', @_), "\n"; +} + +1; +} +# ########################################################################### +# End VersionCheck package +# ########################################################################### + +# ########################################################################### +# HTTPMicro package +# This package is a copy without comments from the original. The original +# with comments and its test file can be found in the Bazaar repository at, +# lib/HTTPMicro.pm +# t/lib/HTTPMicro.t +# See https://launchpad.net/percona-toolkit for more information. +# ########################################################################### +{ + +package HTTPMicro; +BEGIN { + $HTTPMicro::VERSION = '0.001'; +} +use strict; +use warnings; + +use Carp (); + + +my @attributes; +BEGIN { + @attributes = qw(agent timeout); + no strict 'refs'; + for my $accessor ( @attributes ) { + *{$accessor} = sub { + @_ > 1 ? $_[0]->{$accessor} = $_[1] : $_[0]->{$accessor}; + }; + } +} + +sub new { + my($class, %args) = @_; + (my $agent = $class) =~ s{::}{-}g; + my $self = { + agent => $agent . "/" . ($class->VERSION || 0), + timeout => 60, + }; + for my $key ( @attributes ) { + $self->{$key} = $args{$key} if exists $args{$key} + } + return bless $self, $class; +} + +my %DefaultPort = ( + http => 80, + https => 443, +); + +sub request { + my ($self, $method, $url, $args) = @_; + @_ == 3 || (@_ == 4 && ref $args eq 'HASH') + or Carp::croak(q/Usage: $http->request(METHOD, URL, [HASHREF])/); + $args ||= {}; # we keep some state in this during _request + + my $response; + for ( 0 .. 1 ) { + $response = eval { $self->_request($method, $url, $args) }; + last unless $@ && $method eq 'GET' + && $@ =~ m{^(?:Socket closed|Unexpected end)}; + } + + if (my $e = "$@") { + $response = { + success => q{}, + status => 599, + reason => 'Internal Exception', + content => $e, + headers => { + 'content-type' => 'text/plain', + 'content-length' => length $e, + } + }; + } + return $response; +} + +sub _request { + my ($self, $method, $url, $args) = @_; + + my ($scheme, $host, $port, $path_query) = $self->_split_url($url); + + my $request = { + method => $method, + scheme => $scheme, + host_port => ($port == $DefaultPort{$scheme} ? $host : "$host:$port"), + uri => $path_query, + headers => {}, + }; + + my $handle = HTTPMicro::Handle->new(timeout => $self->{timeout}); + + $handle->connect($scheme, $host, $port); + + $self->_prepare_headers_and_cb($request, $args); + $handle->write_request_header(@{$request}{qw/method uri headers/}); + $handle->write_content_body($request) if $request->{content}; + + my $response; + do { $response = $handle->read_response_header } + until (substr($response->{status},0,1) ne '1'); + + if (!($method eq 'HEAD' || $response->{status} =~ /^[23]04/)) { + $response->{content} = ''; + $handle->read_content_body(sub { $_[1]->{content} .= $_[0] }, $response); + } + + $handle->close; + $response->{success} = substr($response->{status},0,1) eq '2'; + return $response; +} + +sub _prepare_headers_and_cb { + my ($self, $request, $args) = @_; + + for ($args->{headers}) { + next unless defined; + while (my ($k, $v) = each %$_) { + $request->{headers}{lc $k} = $v; + } + } + $request->{headers}{'host'} = $request->{host_port}; + $request->{headers}{'connection'} = "close"; + $request->{headers}{'user-agent'} ||= $self->{agent}; + + if (defined $args->{content}) { + $request->{headers}{'content-type'} ||= "application/octet-stream"; + utf8::downgrade($args->{content}, 1) + or Carp::croak(q/Wide character in request message body/); + $request->{headers}{'content-length'} = length $args->{content}; + $request->{content} = $args->{content}; + } + return; +} + +sub _split_url { + my $url = pop; + + my ($scheme, $authority, $path_query) = $url =~ m<\A([^:/?#]+)://([^/?#]*)([^#]*)> + or Carp::croak(qq/Cannot parse URL: '$url'/); + + $scheme = lc $scheme; + $path_query = "/$path_query" unless $path_query =~ m<\A/>; + + my $host = (length($authority)) ? lc $authority : 'localhost'; + $host =~ s/\A[^@]*@//; # userinfo + my $port = do { + $host =~ s/:([0-9]*)\z// && length $1 + ? $1 + : $DefaultPort{$scheme} + }; + + return ($scheme, $host, $port, $path_query); +} + +package + HTTPMicro::Handle; # hide from PAUSE/indexers +use strict; +use warnings; + +use Carp qw[croak]; +use Errno qw[EINTR EPIPE]; +use IO::Socket qw[SOCK_STREAM]; + +sub BUFSIZE () { 32768 } + +my $Printable = sub { + local $_ = shift; + s/\r/\\r/g; + s/\n/\\n/g; + s/\t/\\t/g; + s/([^\x20-\x7E])/sprintf('\\x%.2X', ord($1))/ge; + $_; +}; + +sub new { + my ($class, %args) = @_; + return bless { + rbuf => '', + timeout => 60, + max_line_size => 16384, + %args + }, $class; +} + +my $ssl_verify_args = { + check_cn => "when_only", + wildcards_in_alt => "anywhere", + wildcards_in_cn => "anywhere" +}; + +sub connect { + @_ == 4 || croak(q/Usage: $handle->connect(scheme, host, port)/); + my ($self, $scheme, $host, $port) = @_; + + if ( $scheme eq 'https' ) { + eval "require IO::Socket::SSL" + unless exists $INC{'IO/Socket/SSL.pm'}; + croak(qq/IO::Socket::SSL must be installed for https support\n/) + unless $INC{'IO/Socket/SSL.pm'}; + } + elsif ( $scheme ne 'http' ) { + croak(qq/Unsupported URL scheme '$scheme'\n/); + } + + $self->{fh} = 'IO::Socket::INET'->new( + PeerHost => $host, + PeerPort => $port, + Proto => 'tcp', + Type => SOCK_STREAM, + Timeout => $self->{timeout} + ) or croak(qq/Could not connect to '$host:$port': $@/); + + binmode($self->{fh}) + or croak(qq/Could not binmode() socket: '$!'/); + + if ( $scheme eq 'https') { + IO::Socket::SSL->start_SSL($self->{fh}); + ref($self->{fh}) eq 'IO::Socket::SSL' + or die(qq/SSL connection failed for $host\n/); + if ( $self->{fh}->can("verify_hostname") ) { + $self->{fh}->verify_hostname( $host, $ssl_verify_args ); + } + else { + my $fh = $self->{fh}; + _verify_hostname_of_cert($host, _peer_certificate($fh), $ssl_verify_args) + or die(qq/SSL certificate not valid for $host\n/); + } + } + + $self->{host} = $host; + $self->{port} = $port; + + return $self; +} + +sub close { + @_ == 1 || croak(q/Usage: $handle->close()/); + my ($self) = @_; + CORE::close($self->{fh}) + or croak(qq/Could not close socket: '$!'/); +} + +sub write { + @_ == 2 || croak(q/Usage: $handle->write(buf)/); + my ($self, $buf) = @_; + + my $len = length $buf; + my $off = 0; + + local $SIG{PIPE} = 'IGNORE'; + + while () { + $self->can_write + or croak(q/Timed out while waiting for socket to become ready for writing/); + my $r = syswrite($self->{fh}, $buf, $len, $off); + if (defined $r) { + $len -= $r; + $off += $r; + last unless $len > 0; + } + elsif ($! == EPIPE) { + croak(qq/Socket closed by remote server: $!/); + } + elsif ($! != EINTR) { + croak(qq/Could not write to socket: '$!'/); + } + } + return $off; +} + +sub read { + @_ == 2 || @_ == 3 || croak(q/Usage: $handle->read(len)/); + my ($self, $len) = @_; + + my $buf = ''; + my $got = length $self->{rbuf}; + + if ($got) { + my $take = ($got < $len) ? $got : $len; + $buf = substr($self->{rbuf}, 0, $take, ''); + $len -= $take; + } + + while ($len > 0) { + $self->can_read + or croak(q/Timed out while waiting for socket to become ready for reading/); + my $r = sysread($self->{fh}, $buf, $len, length $buf); + if (defined $r) { + last unless $r; + $len -= $r; + } + elsif ($! != EINTR) { + croak(qq/Could not read from socket: '$!'/); + } + } + if ($len) { + croak(q/Unexpected end of stream/); + } + return $buf; +} + +sub readline { + @_ == 1 || croak(q/Usage: $handle->readline()/); + my ($self) = @_; + + while () { + if ($self->{rbuf} =~ s/\A ([^\x0D\x0A]* \x0D?\x0A)//x) { + return $1; + } + $self->can_read + or croak(q/Timed out while waiting for socket to become ready for reading/); + my $r = sysread($self->{fh}, $self->{rbuf}, BUFSIZE, length $self->{rbuf}); + if (defined $r) { + last unless $r; + } + elsif ($! != EINTR) { + croak(qq/Could not read from socket: '$!'/); + } + } + croak(q/Unexpected end of stream while looking for line/); +} + +sub read_header_lines { + @_ == 1 || @_ == 2 || croak(q/Usage: $handle->read_header_lines([headers])/); + my ($self, $headers) = @_; + $headers ||= {}; + my $lines = 0; + my $val; + + while () { + my $line = $self->readline; + + if ($line =~ /\A ([^\x00-\x1F\x7F:]+) : [\x09\x20]* ([^\x0D\x0A]*)/x) { + my ($field_name) = lc $1; + $val = \($headers->{$field_name} = $2); + } + elsif ($line =~ /\A [\x09\x20]+ ([^\x0D\x0A]*)/x) { + $val + or croak(q/Unexpected header continuation line/); + next unless length $1; + $$val .= ' ' if length $$val; + $$val .= $1; + } + elsif ($line =~ /\A \x0D?\x0A \z/x) { + last; + } + else { + croak(q/Malformed header line: / . $Printable->($line)); + } + } + return $headers; +} + +sub write_header_lines { + (@_ == 2 && ref $_[1] eq 'HASH') || croak(q/Usage: $handle->write_header_lines(headers)/); + my($self, $headers) = @_; + + my $buf = ''; + while (my ($k, $v) = each %$headers) { + my $field_name = lc $k; + $field_name =~ /\A [\x21\x23-\x27\x2A\x2B\x2D\x2E\x30-\x39\x41-\x5A\x5E-\x7A\x7C\x7E]+ \z/x + or croak(q/Invalid HTTP header field name: / . $Printable->($field_name)); + $field_name =~ s/\b(\w)/\u$1/g; + $buf .= "$field_name: $v\x0D\x0A"; + } + $buf .= "\x0D\x0A"; + return $self->write($buf); +} + +sub read_content_body { + @_ == 3 || @_ == 4 || croak(q/Usage: $handle->read_content_body(callback, response, [read_length])/); + my ($self, $cb, $response, $len) = @_; + $len ||= $response->{headers}{'content-length'}; + + croak("No content-length in the returned response, and this " + . "UA doesn't implement chunking") unless defined $len; + + while ($len > 0) { + my $read = ($len > BUFSIZE) ? BUFSIZE : $len; + $cb->($self->read($read), $response); + $len -= $read; + } + + return; +} + +sub write_content_body { + @_ == 2 || croak(q/Usage: $handle->write_content_body(request)/); + my ($self, $request) = @_; + my ($len, $content_length) = (0, $request->{headers}{'content-length'}); + + $len += $self->write($request->{content}); + + $len == $content_length + or croak(qq/Content-Length missmatch (got: $len expected: $content_length)/); + + return $len; +} + +sub read_response_header { + @_ == 1 || croak(q/Usage: $handle->read_response_header()/); + my ($self) = @_; + + my $line = $self->readline; + + $line =~ /\A (HTTP\/(0*\d+\.0*\d+)) [\x09\x20]+ ([0-9]{3}) [\x09\x20]+ ([^\x0D\x0A]*) \x0D?\x0A/x + or croak(q/Malformed Status-Line: / . $Printable->($line)); + + my ($protocol, $version, $status, $reason) = ($1, $2, $3, $4); + + return { + status => $status, + reason => $reason, + headers => $self->read_header_lines, + protocol => $protocol, + }; +} + +sub write_request_header { + @_ == 4 || croak(q/Usage: $handle->write_request_header(method, request_uri, headers)/); + my ($self, $method, $request_uri, $headers) = @_; + + return $self->write("$method $request_uri HTTP/1.1\x0D\x0A") + + $self->write_header_lines($headers); +} + +sub _do_timeout { + my ($self, $type, $timeout) = @_; + $timeout = $self->{timeout} + unless defined $timeout && $timeout >= 0; + + my $fd = fileno $self->{fh}; + defined $fd && $fd >= 0 + or croak(q/select(2): 'Bad file descriptor'/); + + my $initial = time; + my $pending = $timeout; + my $nfound; + + vec(my $fdset = '', $fd, 1) = 1; + + while () { + $nfound = ($type eq 'read') + ? select($fdset, undef, undef, $pending) + : select(undef, $fdset, undef, $pending) ; + if ($nfound == -1) { + $! == EINTR + or croak(qq/select(2): '$!'/); + redo if !$timeout || ($pending = $timeout - (time - $initial)) > 0; + $nfound = 0; + } + last; + } + $! = 0; + return $nfound; +} + +sub can_read { + @_ == 1 || @_ == 2 || croak(q/Usage: $handle->can_read([timeout])/); + my $self = shift; + return $self->_do_timeout('read', @_) +} + +sub can_write { + @_ == 1 || @_ == 2 || croak(q/Usage: $handle->can_write([timeout])/); + my $self = shift; + return $self->_do_timeout('write', @_) +} + +my $prog = <<'EOP'; +BEGIN { + if ( defined &IO::Socket::SSL::CAN_IPV6 ) { + *CAN_IPV6 = \*IO::Socket::SSL::CAN_IPV6; + } + else { + constant->import( CAN_IPV6 => '' ); + } + my %const = ( + NID_CommonName => 13, + GEN_DNS => 2, + GEN_IPADD => 7, + ); + while ( my ($name,$value) = each %const ) { + no strict 'refs'; + *{$name} = UNIVERSAL::can( 'Net::SSLeay', $name ) || sub { $value }; + } +} +{ + my %dispatcher = ( + issuer => sub { Net::SSLeay::X509_NAME_oneline( Net::SSLeay::X509_get_issuer_name( shift )) }, + subject => sub { Net::SSLeay::X509_NAME_oneline( Net::SSLeay::X509_get_subject_name( shift )) }, + ); + if ( $Net::SSLeay::VERSION >= 1.30 ) { + $dispatcher{commonName} = sub { + my $cn = Net::SSLeay::X509_NAME_get_text_by_NID( + Net::SSLeay::X509_get_subject_name( shift ), NID_CommonName); + $cn =~s{\0$}{}; # work around Bug in Net::SSLeay <1.33 + $cn; + } + } else { + $dispatcher{commonName} = sub { + croak "you need at least Net::SSLeay version 1.30 for getting commonName" + } + } + + if ( $Net::SSLeay::VERSION >= 1.33 ) { + $dispatcher{subjectAltNames} = sub { Net::SSLeay::X509_get_subjectAltNames( shift ) }; + } else { + $dispatcher{subjectAltNames} = sub { + return; + }; + } + + $dispatcher{authority} = $dispatcher{issuer}; + $dispatcher{owner} = $dispatcher{subject}; + $dispatcher{cn} = $dispatcher{commonName}; + + sub _peer_certificate { + my ($self, $field) = @_; + my $ssl = $self->_get_ssl_object or return; + + my $cert = ${*$self}{_SSL_certificate} + ||= Net::SSLeay::get_peer_certificate($ssl) + or return $self->error("Could not retrieve peer certificate"); + + if ($field) { + my $sub = $dispatcher{$field} or croak + "invalid argument for peer_certificate, valid are: ".join( " ",keys %dispatcher ). + "\nMaybe you need to upgrade your Net::SSLeay"; + return $sub->($cert); + } else { + return $cert + } + } + + + my %scheme = ( + ldap => { + wildcards_in_cn => 0, + wildcards_in_alt => 'leftmost', + check_cn => 'always', + }, + http => { + wildcards_in_cn => 'anywhere', + wildcards_in_alt => 'anywhere', + check_cn => 'when_only', + }, + smtp => { + wildcards_in_cn => 0, + wildcards_in_alt => 0, + check_cn => 'always' + }, + none => {}, # do not check + ); + + $scheme{www} = $scheme{http}; # alias + $scheme{xmpp} = $scheme{http}; # rfc 3920 + $scheme{pop3} = $scheme{ldap}; # rfc 2595 + $scheme{imap} = $scheme{ldap}; # rfc 2595 + $scheme{acap} = $scheme{ldap}; # rfc 2595 + $scheme{nntp} = $scheme{ldap}; # rfc 4642 + $scheme{ftp} = $scheme{http}; # rfc 4217 + + + sub _verify_hostname_of_cert { + my $identity = shift; + my $cert = shift; + my $scheme = shift || 'none'; + if ( ! ref($scheme) ) { + $scheme = $scheme{$scheme} or croak "scheme $scheme not defined"; + } + + return 1 if ! %$scheme; # 'none' + + my $commonName = $dispatcher{cn}->($cert); + my @altNames = $dispatcher{subjectAltNames}->($cert); + + if ( my $sub = $scheme->{callback} ) { + return $sub->($identity,$commonName,@altNames); + } + + + my $ipn; + if ( CAN_IPV6 and $identity =~m{:} ) { + $ipn = IO::Socket::SSL::inet_pton(IO::Socket::SSL::AF_INET6,$identity) + or croak "'$identity' is not IPv6, but neither IPv4 nor hostname"; + } elsif ( $identity =~m{^\d+\.\d+\.\d+\.\d+$} ) { + $ipn = IO::Socket::SSL::inet_aton( $identity ) or croak "'$identity' is not IPv4, but neither IPv6 nor hostname"; + } else { + if ( $identity =~m{[^a-zA-Z0-9_.\-]} ) { + $identity =~m{\0} and croak("name '$identity' has \\0 byte"); + $identity = IO::Socket::SSL::idn_to_ascii($identity) or + croak "Warning: Given name '$identity' could not be converted to IDNA!"; + } + } + + my $check_name = sub { + my ($name,$identity,$wtyp) = @_; + $wtyp ||= ''; + my $pattern; + if ( $wtyp eq 'anywhere' and $name =~m{^([a-zA-Z0-9_\-]*)\*(.+)} ) { + $pattern = qr{^\Q$1\E[a-zA-Z0-9_\-]*\Q$2\E$}i; + } elsif ( $wtyp eq 'leftmost' and $name =~m{^\*(\..+)$} ) { + $pattern = qr{^[a-zA-Z0-9_\-]*\Q$1\E$}i; + } else { + $pattern = qr{^\Q$name\E$}i; + } + return $identity =~ $pattern; + }; + + my $alt_dnsNames = 0; + while (@altNames) { + my ($type, $name) = splice (@altNames, 0, 2); + if ( $ipn and $type == GEN_IPADD ) { + return 1 if $ipn eq $name; + + } elsif ( ! $ipn and $type == GEN_DNS ) { + $name =~s/\s+$//; $name =~s/^\s+//; + $alt_dnsNames++; + $check_name->($name,$identity,$scheme->{wildcards_in_alt}) + and return 1; + } + } + + if ( ! $ipn and ( + $scheme->{check_cn} eq 'always' or + $scheme->{check_cn} eq 'when_only' and !$alt_dnsNames)) { + $check_name->($commonName,$identity,$scheme->{wildcards_in_cn}) + and return 1; + } + + return 0; # no match + } +} +EOP + +eval { require IO::Socket::SSL }; +if ( $INC{"IO/Socket/SSL.pm"} ) { + eval $prog; + die $@ if $@; +} + +1; +} +# ########################################################################### +# End HTTPMicro package +# ########################################################################### + +# ########################################################################### +# Pingback package +# This package is a copy without comments from the original. The original +# with comments and its test file can be found in the Bazaar repository at, +# lib/Pingback.pm +# t/lib/Pingback.t +# See https://launchpad.net/percona-toolkit for more information. +# ########################################################################### +{ +package Pingback; + +use strict; +use warnings FATAL => 'all'; +use English qw(-no_match_vars); + +use constant PTDEBUG => $ENV{PTDEBUG} || 0; + +use Data::Dumper qw(); +use Digest::MD5 qw(md5_hex); +use Sys::Hostname qw(hostname); +use Fcntl qw(:DEFAULT); +use File::Basename qw(); +use File::Spec; + +my $dir = File::Spec->tmpdir(); +my $check_time_file = File::Spec->catfile($dir,'percona-toolkit-version-check'); +my $check_time_limit = 60 * 60 * 24; # one day + +sub Dumper { + local $Data::Dumper::Indent = 1; + local $Data::Dumper::Sortkeys = 1; + local $Data::Dumper::Quotekeys = 0; + + Data::Dumper::Dumper(@_); +} + +local $EVAL_ERROR; +eval { + require Percona::Toolkit; + require HTTPMicro; + require VersionCheck; +}; + +sub version_check { + my %args = @_; + my @instances = $args{instances} ? @{ $args{instances} } : (); + + if (exists $ENV{PERCONA_VERSION_CHECK} && !$ENV{PERCONA_VERSION_CHECK}) { + warn '--version-check is disabled by the PERCONA_VERSION_CHECK ', + "environment variable.\n\n"; + return; + } + + $args{protocol} ||= 'https'; + my @protocols = $args{protocol} eq 'auto' + ? qw(https http) + : $args{protocol}; + + my $instances_to_check = []; + my $time = int(time()); + eval { + foreach my $instance ( @instances ) { + my ($name, $id) = _generate_identifier($instance); + $instance->{name} = $name; + $instance->{id} = $id; + } + + my $time_to_check; + ($time_to_check, $instances_to_check) + = time_to_check($check_time_file, \@instances, $time); + if ( !$time_to_check ) { + warn 'It is not time to --version-check again; ', + "only 1 check per day.\n\n"; + return; + } + + my $advice; + my $e; + for my $protocol ( @protocols ) { + $advice = eval { pingback( + url => $ENV{PERCONA_VERSION_CHECK_URL} || "$protocol://v.percona.com", + instances => $instances_to_check, + protocol => $protocol, + ) }; + last if !$advice && !$EVAL_ERROR; + $e ||= $EVAL_ERROR; + } + if ( $advice ) { + print "# Percona suggests these upgrades:\n"; + print join("\n", map { "# * $_" } @$advice), "\n\n"; + } + else { + die $e if $e; + print "# No suggestions at this time.\n\n"; + ($ENV{PTVCDEBUG} || PTDEBUG ) + && _d('--version-check worked, but there were no suggestions'); + } + }; + if ( $EVAL_ERROR ) { + warn "Error doing --version-check: $EVAL_ERROR"; + } + else { + update_checks_file($check_time_file, $instances_to_check, $time); + } + + return; +} + +sub pingback { + my (%args) = @_; + my @required_args = qw(url); + foreach my $arg ( @required_args ) { + die "I need a $arg arugment" unless $args{$arg}; + } + my ($url) = @args{@required_args}; + + my ($instances, $ua, $vc) = @args{qw(instances ua VersionCheck)}; + + $ua ||= HTTPMicro->new( timeout => 5 ); + $vc ||= VersionCheck->new(); + + my $response = $ua->request('GET', $url); + ($ENV{PTVCDEBUG} || PTDEBUG) && _d('Server response:', Dumper($response)); + die "No response from GET $url" + if !$response; + die("GET on $url returned HTTP status $response->{status}; expected 200\n", + ($response->{content} || '')) if $response->{status} != 200; + die("GET on $url did not return any programs to check") + if !$response->{content}; + + my $items = $vc->parse_server_response( + response => $response->{content} + ); + die "Failed to parse server requested programs: $response->{content}" + if !scalar keys %$items; + + my $versions = $vc->get_versions( + items => $items, + instances => $instances, + ); + die "Failed to get any program versions; should have at least gotten Perl" + if !scalar keys %$versions; + + my $client_content = encode_client_response( + items => $items, + versions => $versions, + general_id => md5_hex( hostname() ), + ); + + my $client_response = { + headers => { "X-Percona-Toolkit-Tool" => File::Basename::basename($0) }, + content => $client_content, + }; + if ( $ENV{PTVCDEBUG} || PTDEBUG ) { + _d('Client response:', Dumper($client_response)); + } + + $response = $ua->request('POST', $url, $client_response); + PTDEBUG && _d('Server suggestions:', Dumper($response)); + die "No response from POST $url $client_response" + if !$response; + die "POST $url returned HTTP status $response->{status}; expected 200" + if $response->{status} != 200; + + return unless $response->{content}; + + $items = $vc->parse_server_response( + response => $response->{content}, + split_vars => 0, + ); + die "Failed to parse server suggestions: $response->{content}" + if !scalar keys %$items; + my @suggestions = map { $_->{vars} } + sort { $a->{item} cmp $b->{item} } + values %$items; + + return \@suggestions; +} + +sub time_to_check { + my ($file, $instances, $time) = @_; + die "I need a file argument" unless $file; + $time ||= int(time()); # current time + + if ( @$instances ) { + my $instances_to_check = instances_to_check($file, $instances, $time); + return scalar @$instances_to_check, $instances_to_check; + } + + return 1 if !-f $file; + + my $mtime = (stat $file)[9]; + if ( !defined $mtime ) { + PTDEBUG && _d('Error getting modified time of', $file); + return 1; + } + PTDEBUG && _d('time=', $time, 'mtime=', $mtime); + if ( ($time - $mtime) > $check_time_limit ) { + return 1; + } + + return 0; +} + +sub instances_to_check { + my ($file, $instances, $time, %args) = @_; + + my $file_contents = ''; + if (open my $fh, '<', $file) { + chomp($file_contents = do { local $/ = undef; <$fh> }); + close $fh; + } + my %cached_instances = $file_contents =~ /^([^,]+),(.+)$/mg; + + my @instances_to_check; + foreach my $instance ( @$instances ) { + my $mtime = $cached_instances{ $instance->{id} }; + if ( !$mtime || (($time - $mtime) > $check_time_limit) ) { + if ( $ENV{PTVCDEBUG} || PTDEBUG ) { + _d('Time to check MySQL instance', $instance->{name}); + } + push @instances_to_check, $instance; + $cached_instances{ $instance->{id} } = $time; + } + } + + if ( $args{update_file} ) { + open my $fh, '>', $file or die "Cannot open $file for writing: $OS_ERROR"; + while ( my ($id, $time) = each %cached_instances ) { + print { $fh } "$id,$time\n"; + } + close $fh or die "Cannot close $file: $OS_ERROR"; + } + + return \@instances_to_check; +} + +sub update_checks_file { + my ($file, $instances, $time) = @_; + + if ( !-f $file ) { + if ( $ENV{PTVCDEBUG} || PTDEBUG ) { + _d('Creating time limit file', $file); + } + _touch($file); + } + + if ( $instances && @$instances ) { + instances_to_check($file, $instances, $time, update_file => 1); + return; + } + + my $mtime = (stat $file)[9]; + if ( !defined $mtime ) { + _touch($file); + return; + } + PTDEBUG && _d('time=', $time, 'mtime=', $mtime); + if ( ($time - $mtime) > $check_time_limit ) { + _touch($file); + return; + } + + return; +} + +sub _touch { + my ($file) = @_; + sysopen my $fh, $file, O_WRONLY|O_CREAT + or die "Cannot create $file : $!"; + close $fh or die "Cannot close $file : $!"; + utime(undef, undef, $file); +} + +sub _generate_identifier { + my $instance = shift; + my $dbh = $instance->{dbh}; + my $dsn = $instance->{dsn}; + + my $sql = q{SELECT CONCAT(@@hostname, @@port)}; + PTDEBUG && _d($sql); + my ($name) = eval { $dbh->selectrow_array($sql) }; + if ( $EVAL_ERROR ) { + PTDEBUG && _d($EVAL_ERROR); + $sql = q{SELECT @@hostname}; + PTDEBUG && _d($sql); + ($name) = eval { $dbh->selectrow_array($sql) }; + if ( $EVAL_ERROR ) { + PTDEBUG && _d($EVAL_ERROR); + $name = ($dsn->{h} || 'localhost') . ($dsn->{P} || 3306); + } + else { + $sql = q{SHOW VARIABLES LIKE 'port'}; + PTDEBUG && _d($sql); + my (undef, $port) = eval { $dbh->selectrow_array($sql) }; + PTDEBUG && _d('port:', $port); + $name .= $port || ''; + } + } + my $id = md5_hex($name); + + if ( $ENV{PTVCDEBUG} || PTDEBUG ) { + _d('MySQL instance', $name, 'is', $id); + } + + return $name, $id; +} + +sub encode_client_response { + my (%args) = @_; + my @required_args = qw(items versions general_id); + foreach my $arg ( @required_args ) { + die "I need a $arg arugment" unless $args{$arg}; + } + my ($items, $versions, $general_id) = @args{@required_args}; + + my @lines; + foreach my $item ( sort keys %$items ) { + next unless exists $versions->{$item}; + if ( ref($versions->{$item}) eq 'HASH' ) { + my $mysql_versions = $versions->{$item}; + for my $id ( sort keys %$mysql_versions ) { + push @lines, join(';', $id, $item, $mysql_versions->{$id}); + } + } + else { + push @lines, join(';', $general_id, $item, $versions->{$item}); + } + } + + my $client_response = join("\n", @lines) . "\n"; + return $client_response; +} + +sub validate_options { + my ($o) = @_; + + return if !$o->got('version-check'); + + my $value = $o->get('version-check'); + my @values = split /, /, + $o->read_para_after(__FILE__, qr/MAGIC_version_check/); + chomp(@values); + + return if grep { $value eq $_ } @values; + $o->save_error("--version-check invalid value $value. Accepted values are " + . join(", ", @values[0..$#values-1]) . " and $values[-1]" ); +} + +sub _d { + my ($package, undef, $line) = caller 0; + @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; } + map { defined $_ ? $_ : 'undef' } + @_; + print STDERR "# $package:$line $PID ", join(' ', @_), "\n"; +} + +1; +} +# ########################################################################### +# End Pingback package +# ########################################################################### + +# ########################################################################### +# DSNParser package +# This package is a copy without comments from the original. The original +# with comments and its test file can be found in the Bazaar repository at, +# lib/DSNParser.pm +# t/lib/DSNParser.t +# See https://launchpad.net/percona-toolkit for more information. +# ########################################################################### +{ +package DSNParser; + +use strict; +use warnings FATAL => 'all'; +use English qw(-no_match_vars); +use constant PTDEBUG => $ENV{PTDEBUG} || 0; + +use Data::Dumper; +$Data::Dumper::Indent = 0; +$Data::Dumper::Quotekeys = 0; + +my $dsn_sep = qr/(? {} # h, P, u, etc. Should come from DSN OPTIONS section in POD. + }; + foreach my $opt ( @{$args{opts}} ) { + if ( !$opt->{key} || !$opt->{desc} ) { + die "Invalid DSN option: ", Dumper($opt); + } + PTDEBUG && _d('DSN option:', + join(', ', + map { "$_=" . (defined $opt->{$_} ? ($opt->{$_} || '') : 'undef') } + keys %$opt + ) + ); + $self->{opts}->{$opt->{key}} = { + dsn => $opt->{dsn}, + desc => $opt->{desc}, + copy => $opt->{copy} || 0, + }; + } + return bless $self, $class; +} + +sub prop { + my ( $self, $prop, $value ) = @_; + if ( @_ > 2 ) { + PTDEBUG && _d('Setting', $prop, 'property'); + $self->{$prop} = $value; + } + return $self->{$prop}; +} + +sub parse { + my ( $self, $dsn, $prev, $defaults ) = @_; + if ( !$dsn ) { + PTDEBUG && _d('No DSN to parse'); + return; + } + PTDEBUG && _d('Parsing', $dsn); + $prev ||= {}; + $defaults ||= {}; + my %given_props; + my %final_props; + my $opts = $self->{opts}; + + foreach my $dsn_part ( split($dsn_sep, $dsn) ) { + $dsn_part =~ s/\\,/,/g; + if ( my ($prop_key, $prop_val) = $dsn_part =~ m/^(.)=(.*)$/ ) { + $given_props{$prop_key} = $prop_val; + } + else { + PTDEBUG && _d('Interpreting', $dsn_part, 'as h=', $dsn_part); + $given_props{h} = $dsn_part; + } + } + + foreach my $key ( keys %$opts ) { + PTDEBUG && _d('Finding value for', $key); + $final_props{$key} = $given_props{$key}; + if ( !defined $final_props{$key} + && defined $prev->{$key} && $opts->{$key}->{copy} ) + { + $final_props{$key} = $prev->{$key}; + PTDEBUG && _d('Copying value for', $key, 'from previous DSN'); + } + if ( !defined $final_props{$key} ) { + $final_props{$key} = $defaults->{$key}; + PTDEBUG && _d('Copying value for', $key, 'from defaults'); + } + } + + foreach my $key ( keys %given_props ) { + die "Unknown DSN option '$key' in '$dsn'. For more details, " + . "please use the --help option, or try 'perldoc $PROGRAM_NAME' " + . "for complete documentation." + unless exists $opts->{$key}; + } + if ( (my $required = $self->prop('required')) ) { + foreach my $key ( keys %$required ) { + die "Missing required DSN option '$key' in '$dsn'. For more details, " + . "please use the --help option, or try 'perldoc $PROGRAM_NAME' " + . "for complete documentation." + unless $final_props{$key}; + } + } + + return \%final_props; +} + +sub parse_options { + my ( $self, $o ) = @_; + die 'I need an OptionParser object' unless ref $o eq 'OptionParser'; + my $dsn_string + = join(',', + map { "$_=".$o->get($_); } + grep { $o->has($_) && $o->get($_) } + keys %{$self->{opts}} + ); + PTDEBUG && _d('DSN string made from options:', $dsn_string); + return $self->parse($dsn_string); +} + +sub as_string { + my ( $self, $dsn, $props ) = @_; + return $dsn unless ref $dsn; + my @keys = $props ? @$props : sort keys %$dsn; + return join(',', + map { "$_=" . ($_ eq 'p' ? '...' : $dsn->{$_}) } + grep { + exists $self->{opts}->{$_} + && exists $dsn->{$_} + && defined $dsn->{$_} + } @keys); +} + +sub usage { + my ( $self ) = @_; + my $usage + = "DSN syntax is key=value[,key=value...] Allowable DSN keys:\n\n" + . " KEY COPY MEANING\n" + . " === ==== =============================================\n"; + my %opts = %{$self->{opts}}; + foreach my $key ( sort keys %opts ) { + $usage .= " $key " + . ($opts{$key}->{copy} ? 'yes ' : 'no ') + . ($opts{$key}->{desc} || '[No description]') + . "\n"; + } + $usage .= "\n If the DSN is a bareword, the word is treated as the 'h' key.\n"; + return $usage; +} + +sub get_cxn_params { + my ( $self, $info ) = @_; + my $dsn; + my %opts = %{$self->{opts}}; + my $driver = $self->prop('dbidriver') || ''; + if ( $driver eq 'Pg' ) { + $dsn = 'DBI:Pg:dbname=' . ( $info->{D} || '' ) . ';' + . join(';', map { "$opts{$_}->{dsn}=$info->{$_}" } + grep { defined $info->{$_} } + qw(h P)); + } + else { + $dsn = 'DBI:mysql:' . ( $info->{D} || '' ) . ';' + . join(';', map { "$opts{$_}->{dsn}=$info->{$_}" } + grep { defined $info->{$_} } + qw(F h P S A)) + . ';mysql_read_default_group=client' + . ($info->{L} ? ';mysql_local_infile=1' : ''); + } + PTDEBUG && _d($dsn); + return ($dsn, $info->{u}, $info->{p}); +} + +sub fill_in_dsn { + my ( $self, $dbh, $dsn ) = @_; + my $vars = $dbh->selectall_hashref('SHOW VARIABLES', 'Variable_name'); + my ($user, $db) = $dbh->selectrow_array('SELECT USER(), DATABASE()'); + $user =~ s/@.*//; + $dsn->{h} ||= $vars->{hostname}->{Value}; + $dsn->{S} ||= $vars->{'socket'}->{Value}; + $dsn->{P} ||= $vars->{port}->{Value}; + $dsn->{u} ||= $user; + $dsn->{D} ||= $db; +} + +sub get_dbh { + my ( $self, $cxn_string, $user, $pass, $opts ) = @_; + $opts ||= {}; + my $defaults = { + AutoCommit => 0, + RaiseError => 1, + PrintError => 0, + ShowErrorStatement => 1, + mysql_enable_utf8 => ($cxn_string =~ m/charset=utf8/i ? 1 : 0), + }; + @{$defaults}{ keys %$opts } = values %$opts; + if (delete $defaults->{L}) { # L for LOAD DATA LOCAL INFILE, our own extension + $defaults->{mysql_local_infile} = 1; + } + + if ( $opts->{mysql_use_result} ) { + $defaults->{mysql_use_result} = 1; + } + + if ( !$have_dbi ) { + die "Cannot connect to MySQL because the Perl DBI module is not " + . "installed or not found. Run 'perl -MDBI' to see the directories " + . "that Perl searches for DBI. If DBI is not installed, try:\n" + . " Debian/Ubuntu apt-get install libdbi-perl\n" + . " RHEL/CentOS yum install perl-DBI\n" + . " OpenSolaris pkg install pkg:/SUNWpmdbi\n"; + + } + + my $dbh; + my $tries = 2; + while ( !$dbh && $tries-- ) { + PTDEBUG && _d($cxn_string, ' ', $user, ' ', $pass, + join(', ', map { "$_=>$defaults->{$_}" } keys %$defaults )); + + $dbh = eval { DBI->connect($cxn_string, $user, $pass, $defaults) }; + + if ( !$dbh && $EVAL_ERROR ) { + if ( $EVAL_ERROR =~ m/locate DBD\/mysql/i ) { + die "Cannot connect to MySQL because the Perl DBD::mysql module is " + . "not installed or not found. Run 'perl -MDBD::mysql' to see " + . "the directories that Perl searches for DBD::mysql. If " + . "DBD::mysql is not installed, try:\n" + . " Debian/Ubuntu apt-get install libdbd-mysql-perl\n" + . " RHEL/CentOS yum install perl-DBD-MySQL\n" + . " OpenSolaris pgk install pkg:/SUNWapu13dbd-mysql\n"; + } + elsif ( $EVAL_ERROR =~ m/not a compiled character set|character set utf8/ ) { + PTDEBUG && _d('Going to try again without utf8 support'); + delete $defaults->{mysql_enable_utf8}; + } + if ( !$tries ) { + die $EVAL_ERROR; + } + } + } + + if ( $cxn_string =~ m/mysql/i ) { + my $sql; + + $sql = 'SELECT @@SQL_MODE'; + PTDEBUG && _d($dbh, $sql); + my ($sql_mode) = eval { $dbh->selectrow_array($sql) }; + if ( $EVAL_ERROR ) { + die "Error getting the current SQL_MODE: $EVAL_ERROR"; + } + + if ( my ($charset) = $cxn_string =~ m/charset=([\w]+)/ ) { + $sql = qq{/*!40101 SET NAMES "$charset"*/}; + PTDEBUG && _d($dbh, ':', $sql); + eval { $dbh->do($sql) }; + if ( $EVAL_ERROR ) { + die "Error setting NAMES to $charset: $EVAL_ERROR"; + } + PTDEBUG && _d('Enabling charset for STDOUT'); + if ( $charset eq 'utf8' ) { + binmode(STDOUT, ':utf8') + or die "Can't binmode(STDOUT, ':utf8'): $OS_ERROR"; + } + else { + binmode(STDOUT) or die "Can't binmode(STDOUT): $OS_ERROR"; + } + } + + if ( my $var = $self->prop('set-vars') ) { + $sql = "SET $var"; + PTDEBUG && _d($dbh, ':', $sql); + eval { $dbh->do($sql) }; + if ( $EVAL_ERROR ) { + die "Error setting $var: $EVAL_ERROR"; + } + } + + $sql = 'SET @@SQL_QUOTE_SHOW_CREATE = 1' + . '/*!40101, @@SQL_MODE=\'NO_AUTO_VALUE_ON_ZERO' + . ($sql_mode ? ",$sql_mode" : '') + . '\'*/'; + PTDEBUG && _d($dbh, $sql); + eval { $dbh->do($sql) }; + if ( $EVAL_ERROR ) { + die "Error setting SQL_QUOTE_SHOW_CREATE, SQL_MODE" + . ($sql_mode ? " and $sql_mode" : '') + . ": $EVAL_ERROR"; + } + } + + PTDEBUG && _d('DBH info: ', + $dbh, + Dumper($dbh->selectrow_hashref( + 'SELECT DATABASE(), CONNECTION_ID(), VERSION()/*!50038 , @@hostname*/')), + 'Connection info:', $dbh->{mysql_hostinfo}, + 'Character set info:', Dumper($dbh->selectall_arrayref( + "SHOW VARIABLES LIKE 'character_set%'", { Slice => {}})), + '$DBD::mysql::VERSION:', $DBD::mysql::VERSION, + '$DBI::VERSION:', $DBI::VERSION, + ); + + return $dbh; +} + +sub get_hostname { + my ( $self, $dbh ) = @_; + if ( my ($host) = ($dbh->{mysql_hostinfo} || '') =~ m/^(\w+) via/ ) { + return $host; + } + my ( $hostname, $one ) = $dbh->selectrow_array( + 'SELECT /*!50038 @@hostname, */ 1'); + return $hostname; +} + +sub disconnect { + my ( $self, $dbh ) = @_; + PTDEBUG && $self->print_active_handles($dbh); + $dbh->disconnect; +} + +sub print_active_handles { + my ( $self, $thing, $level ) = @_; + $level ||= 0; + printf("# Active %sh: %s %s %s\n", ($thing->{Type} || 'undef'), "\t" x $level, + $thing, (($thing->{Type} || '') eq 'st' ? $thing->{Statement} || '' : '')) + or die "Cannot print: $OS_ERROR"; + foreach my $handle ( grep {defined} @{ $thing->{ChildHandles} } ) { + $self->print_active_handles( $handle, $level + 1 ); + } +} + +sub copy { + my ( $self, $dsn_1, $dsn_2, %args ) = @_; + die 'I need a dsn_1 argument' unless $dsn_1; + die 'I need a dsn_2 argument' unless $dsn_2; + my %new_dsn = map { + my $key = $_; + my $val; + if ( $args{overwrite} ) { + $val = defined $dsn_1->{$key} ? $dsn_1->{$key} : $dsn_2->{$key}; + } + else { + $val = defined $dsn_2->{$key} ? $dsn_2->{$key} : $dsn_1->{$key}; + } + $key => $val; + } keys %{$self->{opts}}; + return \%new_dsn; +} + +sub _d { + my ($package, undef, $line) = caller 0; + @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; } + map { defined $_ ? $_ : 'undef' } + @_; + print STDERR "# $package:$line $PID ", join(' ', @_), "\n"; +} + +1; +} +# ########################################################################### +# End DSNParser package +# ########################################################################### + +# ########################################################################### +# OptionParser package +# This package is a copy without comments from the original. The original +# with comments and its test file can be found in the Bazaar repository at, +# lib/OptionParser.pm +# t/lib/OptionParser.t +# See https://launchpad.net/percona-toolkit for more information. +# ########################################################################### +{ +package OptionParser; + +use strict; +use warnings FATAL => 'all'; +use English qw(-no_match_vars); +use constant PTDEBUG => $ENV{PTDEBUG} || 0; + +use List::Util qw(max); +use Getopt::Long; + +my $POD_link_re = '[LC]<"?([^">]+)"?>'; + +sub new { + my ( $class, %args ) = @_; + my @required_args = qw(); + foreach my $arg ( @required_args ) { + die "I need a $arg argument" unless $args{$arg}; + } + + my ($program_name) = $PROGRAM_NAME =~ m/([.A-Za-z-]+)$/; + $program_name ||= $PROGRAM_NAME; + my $home = $ENV{HOME} || $ENV{HOMEPATH} || $ENV{USERPROFILE} || '.'; + + my %attributes = ( + 'type' => 1, + 'short form' => 1, + 'group' => 1, + 'default' => 1, + 'cumulative' => 1, + 'negatable' => 1, + ); + + my $self = { + head1 => 'OPTIONS', # These args are used internally + skip_rules => 0, # to instantiate another Option- + item => '--(.*)', # Parser obj that parses the + attributes => \%attributes, # DSN OPTIONS section. Tools + parse_attributes => \&_parse_attribs, # don't tinker with these args. + + %args, + + strict => 1, # disabled by a special rule + program_name => $program_name, + opts => {}, + got_opts => 0, + short_opts => {}, + defaults => {}, + groups => {}, + allowed_groups => {}, + errors => [], + rules => [], # desc of rules for --help + mutex => [], # rule: opts are mutually exclusive + atleast1 => [], # rule: at least one opt is required + disables => {}, # rule: opt disables other opts + defaults_to => {}, # rule: opt defaults to value of other opt + DSNParser => undef, + default_files => [ + "/etc/percona-toolkit/percona-toolkit.conf", + "/etc/percona-toolkit/$program_name.conf", + "$home/.percona-toolkit.conf", + "$home/.$program_name.conf", + ], + types => { + string => 's', # standard Getopt type + int => 'i', # standard Getopt type + float => 'f', # standard Getopt type + Hash => 'H', # hash, formed from a comma-separated list + hash => 'h', # hash as above, but only if a value is given + Array => 'A', # array, similar to Hash + array => 'a', # array, similar to hash + DSN => 'd', # DSN + size => 'z', # size with kMG suffix (powers of 2^10) + time => 'm', # time, with an optional suffix of s/h/m/d + }, + }; + + return bless $self, $class; +} + +sub get_specs { + my ( $self, $file ) = @_; + $file ||= $self->{file} || __FILE__; + my @specs = $self->_pod_to_specs($file); + $self->_parse_specs(@specs); + + open my $fh, "<", $file or die "Cannot open $file: $OS_ERROR"; + my $contents = do { local $/ = undef; <$fh> }; + close $fh; + if ( $contents =~ m/^=head1 DSN OPTIONS/m ) { + PTDEBUG && _d('Parsing DSN OPTIONS'); + my $dsn_attribs = { + dsn => 1, + copy => 1, + }; + my $parse_dsn_attribs = sub { + my ( $self, $option, $attribs ) = @_; + map { + my $val = $attribs->{$_}; + if ( $val ) { + $val = $val eq 'yes' ? 1 + : $val eq 'no' ? 0 + : $val; + $attribs->{$_} = $val; + } + } keys %$attribs; + return { + key => $option, + %$attribs, + }; + }; + my $dsn_o = new OptionParser( + description => 'DSN OPTIONS', + head1 => 'DSN OPTIONS', + dsn => 0, # XXX don't infinitely recurse! + item => '\* (.)', # key opts are a single character + skip_rules => 1, # no rules before opts + attributes => $dsn_attribs, + parse_attributes => $parse_dsn_attribs, + ); + my @dsn_opts = map { + my $opts = { + key => $_->{spec}->{key}, + dsn => $_->{spec}->{dsn}, + copy => $_->{spec}->{copy}, + desc => $_->{desc}, + }; + $opts; + } $dsn_o->_pod_to_specs($file); + $self->{DSNParser} = DSNParser->new(opts => \@dsn_opts); + } + + if ( $contents =~ m/^=head1 VERSION\n\n^(.+)$/m ) { + $self->{version} = $1; + PTDEBUG && _d($self->{version}); + } + + return; +} + +sub DSNParser { + my ( $self ) = @_; + return $self->{DSNParser}; +}; + +sub get_defaults_files { + my ( $self ) = @_; + return @{$self->{default_files}}; +} + +sub _pod_to_specs { + my ( $self, $file ) = @_; + $file ||= $self->{file} || __FILE__; + open my $fh, '<', $file or die "Cannot open $file: $OS_ERROR"; + + my @specs = (); + my @rules = (); + my $para; + + local $INPUT_RECORD_SEPARATOR = ''; + while ( $para = <$fh> ) { + next unless $para =~ m/^=head1 $self->{head1}/; + last; + } + + while ( $para = <$fh> ) { + last if $para =~ m/^=over/; + next if $self->{skip_rules}; + chomp $para; + $para =~ s/\s+/ /g; + $para =~ s/$POD_link_re/$1/go; + PTDEBUG && _d('Option rule:', $para); + push @rules, $para; + } + + die "POD has no $self->{head1} section" unless $para; + + do { + if ( my ($option) = $para =~ m/^=item $self->{item}/ ) { + chomp $para; + PTDEBUG && _d($para); + my %attribs; + + $para = <$fh>; # read next paragraph, possibly attributes + + if ( $para =~ m/: / ) { # attributes + $para =~ s/\s+\Z//g; + %attribs = map { + my ( $attrib, $val) = split(/: /, $_); + die "Unrecognized attribute for --$option: $attrib" + unless $self->{attributes}->{$attrib}; + ($attrib, $val); + } split(/; /, $para); + if ( $attribs{'short form'} ) { + $attribs{'short form'} =~ s/-//; + } + $para = <$fh>; # read next paragraph, probably short help desc + } + else { + PTDEBUG && _d('Option has no attributes'); + } + + $para =~ s/\s+\Z//g; + $para =~ s/\s+/ /g; + $para =~ s/$POD_link_re/$1/go; + + $para =~ s/\.(?:\n.*| [A-Z].*|\Z)//s; + PTDEBUG && _d('Short help:', $para); + + die "No description after option spec $option" if $para =~ m/^=item/; + + if ( my ($base_option) = $option =~ m/^\[no\](.*)/ ) { + $option = $base_option; + $attribs{'negatable'} = 1; + } + + push @specs, { + spec => $self->{parse_attributes}->($self, $option, \%attribs), + desc => $para + . (defined $attribs{default} ? " (default $attribs{default})" : ''), + group => ($attribs{'group'} ? $attribs{'group'} : 'default'), + }; + } + while ( $para = <$fh> ) { + last unless $para; + if ( $para =~ m/^=head1/ ) { + $para = undef; # Can't 'last' out of a do {} block. + last; + } + last if $para =~ m/^=item /; + } + } while ( $para ); + + die "No valid specs in $self->{head1}" unless @specs; + + close $fh; + return @specs, @rules; +} + +sub _parse_specs { + my ( $self, @specs ) = @_; + my %disables; # special rule that requires deferred checking + + foreach my $opt ( @specs ) { + if ( ref $opt ) { # It's an option spec, not a rule. + PTDEBUG && _d('Parsing opt spec:', + map { ($_, '=>', $opt->{$_}) } keys %$opt); + + my ( $long, $short ) = $opt->{spec} =~ m/^([\w-]+)(?:\|([^!+=]*))?/; + if ( !$long ) { + die "Cannot parse long option from spec $opt->{spec}"; + } + $opt->{long} = $long; + + die "Duplicate long option --$long" if exists $self->{opts}->{$long}; + $self->{opts}->{$long} = $opt; + + if ( length $long == 1 ) { + PTDEBUG && _d('Long opt', $long, 'looks like short opt'); + $self->{short_opts}->{$long} = $long; + } + + if ( $short ) { + die "Duplicate short option -$short" + if exists $self->{short_opts}->{$short}; + $self->{short_opts}->{$short} = $long; + $opt->{short} = $short; + } + else { + $opt->{short} = undef; + } + + $opt->{is_negatable} = $opt->{spec} =~ m/!/ ? 1 : 0; + $opt->{is_cumulative} = $opt->{spec} =~ m/\+/ ? 1 : 0; + $opt->{is_required} = $opt->{desc} =~ m/required/ ? 1 : 0; + + $opt->{group} ||= 'default'; + $self->{groups}->{ $opt->{group} }->{$long} = 1; + + $opt->{value} = undef; + $opt->{got} = 0; + + my ( $type ) = $opt->{spec} =~ m/=(.)/; + $opt->{type} = $type; + PTDEBUG && _d($long, 'type:', $type); + + + $opt->{spec} =~ s/=./=s/ if ( $type && $type =~ m/[HhAadzm]/ ); + + if ( (my ($def) = $opt->{desc} =~ m/default\b(?: ([^)]+))?/) ) { + $self->{defaults}->{$long} = defined $def ? $def : 1; + PTDEBUG && _d($long, 'default:', $def); + } + + if ( $long eq 'config' ) { + $self->{defaults}->{$long} = join(',', $self->get_defaults_files()); + } + + if ( (my ($dis) = $opt->{desc} =~ m/(disables .*)/) ) { + $disables{$long} = $dis; + PTDEBUG && _d('Deferring check of disables rule for', $opt, $dis); + } + + $self->{opts}->{$long} = $opt; + } + else { # It's an option rule, not a spec. + PTDEBUG && _d('Parsing rule:', $opt); + push @{$self->{rules}}, $opt; + my @participants = $self->_get_participants($opt); + my $rule_ok = 0; + + if ( $opt =~ m/mutually exclusive|one and only one/ ) { + $rule_ok = 1; + push @{$self->{mutex}}, \@participants; + PTDEBUG && _d(@participants, 'are mutually exclusive'); + } + if ( $opt =~ m/at least one|one and only one/ ) { + $rule_ok = 1; + push @{$self->{atleast1}}, \@participants; + PTDEBUG && _d(@participants, 'require at least one'); + } + if ( $opt =~ m/default to/ ) { + $rule_ok = 1; + $self->{defaults_to}->{$participants[0]} = $participants[1]; + PTDEBUG && _d($participants[0], 'defaults to', $participants[1]); + } + if ( $opt =~ m/restricted to option groups/ ) { + $rule_ok = 1; + my ($groups) = $opt =~ m/groups ([\w\s\,]+)/; + my @groups = split(',', $groups); + %{$self->{allowed_groups}->{$participants[0]}} = map { + s/\s+//; + $_ => 1; + } @groups; + } + if( $opt =~ m/accepts additional command-line arguments/ ) { + $rule_ok = 1; + $self->{strict} = 0; + PTDEBUG && _d("Strict mode disabled by rule"); + } + + die "Unrecognized option rule: $opt" unless $rule_ok; + } + } + + foreach my $long ( keys %disables ) { + my @participants = $self->_get_participants($disables{$long}); + $self->{disables}->{$long} = \@participants; + PTDEBUG && _d('Option', $long, 'disables', @participants); + } + + return; +} + +sub _get_participants { + my ( $self, $str ) = @_; + my @participants; + foreach my $long ( $str =~ m/--(?:\[no\])?([\w-]+)/g ) { + die "Option --$long does not exist while processing rule $str" + unless exists $self->{opts}->{$long}; + push @participants, $long; + } + PTDEBUG && _d('Participants for', $str, ':', @participants); + return @participants; +} + +sub opts { + my ( $self ) = @_; + my %opts = %{$self->{opts}}; + return %opts; +} + +sub short_opts { + my ( $self ) = @_; + my %short_opts = %{$self->{short_opts}}; + return %short_opts; +} + +sub set_defaults { + my ( $self, %defaults ) = @_; + $self->{defaults} = {}; + foreach my $long ( keys %defaults ) { + die "Cannot set default for nonexistent option $long" + unless exists $self->{opts}->{$long}; + $self->{defaults}->{$long} = $defaults{$long}; + PTDEBUG && _d('Default val for', $long, ':', $defaults{$long}); + } + return; +} + +sub get_defaults { + my ( $self ) = @_; + return $self->{defaults}; +} + +sub get_groups { + my ( $self ) = @_; + return $self->{groups}; +} + +sub _set_option { + my ( $self, $opt, $val ) = @_; + my $long = exists $self->{opts}->{$opt} ? $opt + : exists $self->{short_opts}->{$opt} ? $self->{short_opts}->{$opt} + : die "Getopt::Long gave a nonexistent option: $opt"; + + $opt = $self->{opts}->{$long}; + if ( $opt->{is_cumulative} ) { + $opt->{value}++; + } + else { + $opt->{value} = $val; + } + $opt->{got} = 1; + PTDEBUG && _d('Got option', $long, '=', $val); +} + +sub get_opts { + my ( $self ) = @_; + + foreach my $long ( keys %{$self->{opts}} ) { + $self->{opts}->{$long}->{got} = 0; + $self->{opts}->{$long}->{value} + = exists $self->{defaults}->{$long} ? $self->{defaults}->{$long} + : $self->{opts}->{$long}->{is_cumulative} ? 0 + : undef; + } + $self->{got_opts} = 0; + + $self->{errors} = []; + + if ( @ARGV && $ARGV[0] eq "--config" ) { + shift @ARGV; + $self->_set_option('config', shift @ARGV); + } + if ( $self->has('config') ) { + my @extra_args; + foreach my $filename ( split(',', $self->get('config')) ) { + eval { + push @extra_args, $self->_read_config_file($filename); + }; + if ( $EVAL_ERROR ) { + if ( $self->got('config') ) { + die $EVAL_ERROR; + } + elsif ( PTDEBUG ) { + _d($EVAL_ERROR); + } + } + } + unshift @ARGV, @extra_args; + } + + Getopt::Long::Configure('no_ignore_case', 'bundling'); + GetOptions( + map { $_->{spec} => sub { $self->_set_option(@_); } } + grep { $_->{long} ne 'config' } # --config is handled specially above. + values %{$self->{opts}} + ) or $self->save_error('Error parsing options'); + + if ( exists $self->{opts}->{version} && $self->{opts}->{version}->{got} ) { + if ( $self->{version} ) { + print $self->{version}, "\n"; + } + else { + print "Error parsing version. See the VERSION section of the tool's documentation.\n"; + } + exit 1; + } + + if ( @ARGV && $self->{strict} ) { + $self->save_error("Unrecognized command-line options @ARGV"); + } + + foreach my $mutex ( @{$self->{mutex}} ) { + my @set = grep { $self->{opts}->{$_}->{got} } @$mutex; + if ( @set > 1 ) { + my $err = join(', ', map { "--$self->{opts}->{$_}->{long}" } + @{$mutex}[ 0 .. scalar(@$mutex) - 2] ) + . ' and --'.$self->{opts}->{$mutex->[-1]}->{long} + . ' are mutually exclusive.'; + $self->save_error($err); + } + } + + foreach my $required ( @{$self->{atleast1}} ) { + my @set = grep { $self->{opts}->{$_}->{got} } @$required; + if ( @set == 0 ) { + my $err = join(', ', map { "--$self->{opts}->{$_}->{long}" } + @{$required}[ 0 .. scalar(@$required) - 2] ) + .' or --'.$self->{opts}->{$required->[-1]}->{long}; + $self->save_error("Specify at least one of $err"); + } + } + + $self->_check_opts( keys %{$self->{opts}} ); + $self->{got_opts} = 1; + return; +} + +sub _check_opts { + my ( $self, @long ) = @_; + my $long_last = scalar @long; + while ( @long ) { + foreach my $i ( 0..$#long ) { + my $long = $long[$i]; + next unless $long; + my $opt = $self->{opts}->{$long}; + if ( $opt->{got} ) { + if ( exists $self->{disables}->{$long} ) { + my @disable_opts = @{$self->{disables}->{$long}}; + map { $self->{opts}->{$_}->{value} = undef; } @disable_opts; + PTDEBUG && _d('Unset options', @disable_opts, + 'because', $long,'disables them'); + } + + if ( exists $self->{allowed_groups}->{$long} ) { + + my @restricted_groups = grep { + !exists $self->{allowed_groups}->{$long}->{$_} + } keys %{$self->{groups}}; + + my @restricted_opts; + foreach my $restricted_group ( @restricted_groups ) { + RESTRICTED_OPT: + foreach my $restricted_opt ( + keys %{$self->{groups}->{$restricted_group}} ) + { + next RESTRICTED_OPT if $restricted_opt eq $long; + push @restricted_opts, $restricted_opt + if $self->{opts}->{$restricted_opt}->{got}; + } + } + + if ( @restricted_opts ) { + my $err; + if ( @restricted_opts == 1 ) { + $err = "--$restricted_opts[0]"; + } + else { + $err = join(', ', + map { "--$self->{opts}->{$_}->{long}" } + grep { $_ } + @restricted_opts[0..scalar(@restricted_opts) - 2] + ) + . ' or --'.$self->{opts}->{$restricted_opts[-1]}->{long}; + } + $self->save_error("--$long is not allowed with $err"); + } + } + + } + elsif ( $opt->{is_required} ) { + $self->save_error("Required option --$long must be specified"); + } + + $self->_validate_type($opt); + if ( $opt->{parsed} ) { + delete $long[$i]; + } + else { + PTDEBUG && _d('Temporarily failed to parse', $long); + } + } + + die "Failed to parse options, possibly due to circular dependencies" + if @long == $long_last; + $long_last = @long; + } + + return; +} + +sub _validate_type { + my ( $self, $opt ) = @_; + return unless $opt; + + if ( !$opt->{type} ) { + $opt->{parsed} = 1; + return; + } + + my $val = $opt->{value}; + + if ( $val && $opt->{type} eq 'm' ) { # type time + PTDEBUG && _d('Parsing option', $opt->{long}, 'as a time value'); + my ( $prefix, $num, $suffix ) = $val =~ m/([+-]?)(\d+)([a-z])?$/; + if ( !$suffix ) { + my ( $s ) = $opt->{desc} =~ m/\(suffix (.)\)/; + $suffix = $s || 's'; + PTDEBUG && _d('No suffix given; using', $suffix, 'for', + $opt->{long}, '(value:', $val, ')'); + } + if ( $suffix =~ m/[smhd]/ ) { + $val = $suffix eq 's' ? $num # Seconds + : $suffix eq 'm' ? $num * 60 # Minutes + : $suffix eq 'h' ? $num * 3600 # Hours + : $num * 86400; # Days + $opt->{value} = ($prefix || '') . $val; + PTDEBUG && _d('Setting option', $opt->{long}, 'to', $val); + } + else { + $self->save_error("Invalid time suffix for --$opt->{long}"); + } + } + elsif ( $val && $opt->{type} eq 'd' ) { # type DSN + PTDEBUG && _d('Parsing option', $opt->{long}, 'as a DSN'); + my $prev = {}; + my $from_key = $self->{defaults_to}->{ $opt->{long} }; + if ( $from_key ) { + PTDEBUG && _d($opt->{long}, 'DSN copies from', $from_key, 'DSN'); + if ( $self->{opts}->{$from_key}->{parsed} ) { + $prev = $self->{opts}->{$from_key}->{value}; + } + else { + PTDEBUG && _d('Cannot parse', $opt->{long}, 'until', + $from_key, 'parsed'); + return; + } + } + my $defaults = $self->{DSNParser}->parse_options($self); + $opt->{value} = $self->{DSNParser}->parse($val, $prev, $defaults); + } + elsif ( $val && $opt->{type} eq 'z' ) { # type size + PTDEBUG && _d('Parsing option', $opt->{long}, 'as a size value'); + $self->_parse_size($opt, $val); + } + elsif ( $opt->{type} eq 'H' || (defined $val && $opt->{type} eq 'h') ) { + $opt->{value} = { map { $_ => 1 } split(/(?{type} eq 'A' || (defined $val && $opt->{type} eq 'a') ) { + $opt->{value} = [ split(/(?{long}, 'type', $opt->{type}, 'value', $val); + } + + $opt->{parsed} = 1; + return; +} + +sub get { + my ( $self, $opt ) = @_; + my $long = (length $opt == 1 ? $self->{short_opts}->{$opt} : $opt); + die "Option $opt does not exist" + unless $long && exists $self->{opts}->{$long}; + return $self->{opts}->{$long}->{value}; +} + +sub got { + my ( $self, $opt ) = @_; + my $long = (length $opt == 1 ? $self->{short_opts}->{$opt} : $opt); + die "Option $opt does not exist" + unless $long && exists $self->{opts}->{$long}; + return $self->{opts}->{$long}->{got}; +} + +sub has { + my ( $self, $opt ) = @_; + my $long = (length $opt == 1 ? $self->{short_opts}->{$opt} : $opt); + return defined $long ? exists $self->{opts}->{$long} : 0; +} + +sub set { + my ( $self, $opt, $val ) = @_; + my $long = (length $opt == 1 ? $self->{short_opts}->{$opt} : $opt); + die "Option $opt does not exist" + unless $long && exists $self->{opts}->{$long}; + $self->{opts}->{$long}->{value} = $val; + return; +} + +sub save_error { + my ( $self, $error ) = @_; + push @{$self->{errors}}, $error; + return; +} + +sub errors { + my ( $self ) = @_; + return $self->{errors}; +} + +sub usage { + my ( $self ) = @_; + warn "No usage string is set" unless $self->{usage}; # XXX + return "Usage: " . ($self->{usage} || '') . "\n"; +} + +sub descr { + my ( $self ) = @_; + warn "No description string is set" unless $self->{description}; # XXX + my $descr = ($self->{description} || $self->{program_name} || '') + . " For more details, please use the --help option, " + . "or try 'perldoc $PROGRAM_NAME' " + . "for complete documentation."; + $descr = join("\n", $descr =~ m/(.{0,80})(?:\s+|$)/g) + unless $ENV{DONT_BREAK_LINES}; + $descr =~ s/ +$//mg; + return $descr; +} + +sub usage_or_errors { + my ( $self, $file, $return ) = @_; + $file ||= $self->{file} || __FILE__; + + if ( !$self->{description} || !$self->{usage} ) { + PTDEBUG && _d("Getting description and usage from SYNOPSIS in", $file); + my %synop = $self->_parse_synopsis($file); + $self->{description} ||= $synop{description}; + $self->{usage} ||= $synop{usage}; + PTDEBUG && _d("Description:", $self->{description}, + "\nUsage:", $self->{usage}); + } + + if ( $self->{opts}->{help}->{got} ) { + print $self->print_usage() or die "Cannot print usage: $OS_ERROR"; + exit 0 unless $return; + } + elsif ( scalar @{$self->{errors}} ) { + print $self->print_errors() or die "Cannot print errors: $OS_ERROR"; + exit 1 unless $return; + } + + return; +} + +sub print_errors { + my ( $self ) = @_; + my $usage = $self->usage() . "\n"; + if ( (my @errors = @{$self->{errors}}) ) { + $usage .= join("\n * ", 'Errors in command-line arguments:', @errors) + . "\n"; + } + return $usage . "\n" . $self->descr(); +} + +sub print_usage { + my ( $self ) = @_; + die "Run get_opts() before print_usage()" unless $self->{got_opts}; + my @opts = values %{$self->{opts}}; + + my $maxl = max( + map { + length($_->{long}) # option long name + + ($_->{is_negatable} ? 4 : 0) # "[no]" if opt is negatable + + ($_->{type} ? 2 : 0) # "=x" where x is the opt type + } + @opts); + + my $maxs = max(0, + map { + length($_) + + ($self->{opts}->{$_}->{is_negatable} ? 4 : 0) + + ($self->{opts}->{$_}->{type} ? 2 : 0) + } + values %{$self->{short_opts}}); + + my $lcol = max($maxl, ($maxs + 3)); + my $rcol = 80 - $lcol - 6; + my $rpad = ' ' x ( 80 - $rcol ); + + $maxs = max($lcol - 3, $maxs); + + my $usage = $self->descr() . "\n" . $self->usage(); + + my @groups = reverse sort grep { $_ ne 'default'; } keys %{$self->{groups}}; + push @groups, 'default'; + + foreach my $group ( reverse @groups ) { + $usage .= "\n".($group eq 'default' ? 'Options' : $group).":\n\n"; + foreach my $opt ( + sort { $a->{long} cmp $b->{long} } + grep { $_->{group} eq $group } + @opts ) + { + my $long = $opt->{is_negatable} ? "[no]$opt->{long}" : $opt->{long}; + my $short = $opt->{short}; + my $desc = $opt->{desc}; + + $long .= $opt->{type} ? "=$opt->{type}" : ""; + + if ( $opt->{type} && $opt->{type} eq 'm' ) { + my ($s) = $desc =~ m/\(suffix (.)\)/; + $s ||= 's'; + $desc =~ s/\s+\(suffix .\)//; + $desc .= ". Optional suffix s=seconds, m=minutes, h=hours, " + . "d=days; if no suffix, $s is used."; + } + $desc = join("\n$rpad", grep { $_ } $desc =~ m/(.{0,$rcol}(?!\W))(?:\s+|(?<=\W)|$)/g); + $desc =~ s/ +$//mg; + if ( $short ) { + $usage .= sprintf(" --%-${maxs}s -%s %s\n", $long, $short, $desc); + } + else { + $usage .= sprintf(" --%-${lcol}s %s\n", $long, $desc); + } + } + } + + $usage .= "\nOption types: s=string, i=integer, f=float, h/H/a/A=comma-separated list, d=DSN, z=size, m=time\n"; + + if ( (my @rules = @{$self->{rules}}) ) { + $usage .= "\nRules:\n\n"; + $usage .= join("\n", map { " $_" } @rules) . "\n"; + } + if ( $self->{DSNParser} ) { + $usage .= "\n" . $self->{DSNParser}->usage(); + } + $usage .= "\nOptions and values after processing arguments:\n\n"; + foreach my $opt ( sort { $a->{long} cmp $b->{long} } @opts ) { + my $val = $opt->{value}; + my $type = $opt->{type} || ''; + my $bool = $opt->{spec} =~ m/^[\w-]+(?:\|[\w-])?!?$/; + $val = $bool ? ( $val ? 'TRUE' : 'FALSE' ) + : !defined $val ? '(No value)' + : $type eq 'd' ? $self->{DSNParser}->as_string($val) + : $type =~ m/H|h/ ? join(',', sort keys %$val) + : $type =~ m/A|a/ ? join(',', @$val) + : $val; + $usage .= sprintf(" --%-${lcol}s %s\n", $opt->{long}, $val); + } + return $usage; +} + +sub prompt_noecho { + shift @_ if ref $_[0] eq __PACKAGE__; + my ( $prompt ) = @_; + local $OUTPUT_AUTOFLUSH = 1; + print $prompt + or die "Cannot print: $OS_ERROR"; + my $response; + eval { + require Term::ReadKey; + Term::ReadKey::ReadMode('noecho'); + chomp($response = ); + Term::ReadKey::ReadMode('normal'); + print "\n" + or die "Cannot print: $OS_ERROR"; + }; + if ( $EVAL_ERROR ) { + die "Cannot read response; is Term::ReadKey installed? $EVAL_ERROR"; + } + return $response; +} + +sub _read_config_file { + my ( $self, $filename ) = @_; + open my $fh, "<", $filename or die "Cannot open $filename: $OS_ERROR\n"; + my @args; + my $prefix = '--'; + my $parse = 1; + + LINE: + while ( my $line = <$fh> ) { + chomp $line; + next LINE if $line =~ m/^\s*(?:\#|\;|$)/; + $line =~ s/\s+#.*$//g; + $line =~ s/^\s+|\s+$//g; + if ( $line eq '--' ) { + $prefix = ''; + $parse = 0; + next LINE; + } + if ( $parse + && (my($opt, $arg) = $line =~ m/^\s*([^=\s]+?)(?:\s*=\s*(.*?)\s*)?$/) + ) { + push @args, grep { defined $_ } ("$prefix$opt", $arg); + } + elsif ( $line =~ m/./ ) { + push @args, $line; + } + else { + die "Syntax error in file $filename at line $INPUT_LINE_NUMBER"; + } + } + close $fh; + return @args; +} + +sub read_para_after { + my ( $self, $file, $regex ) = @_; + open my $fh, "<", $file or die "Can't open $file: $OS_ERROR"; + local $INPUT_RECORD_SEPARATOR = ''; + my $para; + while ( $para = <$fh> ) { + next unless $para =~ m/^=pod$/m; + last; + } + while ( $para = <$fh> ) { + next unless $para =~ m/$regex/; + last; + } + $para = <$fh>; + chomp($para); + close $fh or die "Can't close $file: $OS_ERROR"; + return $para; +} + +sub clone { + my ( $self ) = @_; + + my %clone = map { + my $hashref = $self->{$_}; + my $val_copy = {}; + foreach my $key ( keys %$hashref ) { + my $ref = ref $hashref->{$key}; + $val_copy->{$key} = !$ref ? $hashref->{$key} + : $ref eq 'HASH' ? { %{$hashref->{$key}} } + : $ref eq 'ARRAY' ? [ @{$hashref->{$key}} ] + : $hashref->{$key}; + } + $_ => $val_copy; + } qw(opts short_opts defaults); + + foreach my $scalar ( qw(got_opts) ) { + $clone{$scalar} = $self->{$scalar}; + } + + return bless \%clone; +} + +sub _parse_size { + my ( $self, $opt, $val ) = @_; + + if ( lc($val || '') eq 'null' ) { + PTDEBUG && _d('NULL size for', $opt->{long}); + $opt->{value} = 'null'; + return; + } + + my %factor_for = (k => 1_024, M => 1_048_576, G => 1_073_741_824); + my ($pre, $num, $factor) = $val =~ m/^([+-])?(\d+)([kMG])?$/; + if ( defined $num ) { + if ( $factor ) { + $num *= $factor_for{$factor}; + PTDEBUG && _d('Setting option', $opt->{y}, + 'to num', $num, '* factor', $factor); + } + $opt->{value} = ($pre || '') . $num; + } + else { + $self->save_error("Invalid size for --$opt->{long}: $val"); + } + return; +} + +sub _parse_attribs { + my ( $self, $option, $attribs ) = @_; + my $types = $self->{types}; + return $option + . ($attribs->{'short form'} ? '|' . $attribs->{'short form'} : '' ) + . ($attribs->{'negatable'} ? '!' : '' ) + . ($attribs->{'cumulative'} ? '+' : '' ) + . ($attribs->{'type'} ? '=' . $types->{$attribs->{type}} : '' ); +} + +sub _parse_synopsis { + my ( $self, $file ) = @_; + $file ||= $self->{file} || __FILE__; + PTDEBUG && _d("Parsing SYNOPSIS in", $file); + + local $INPUT_RECORD_SEPARATOR = ''; # read paragraphs + open my $fh, "<", $file or die "Cannot open $file: $OS_ERROR"; + my $para; + 1 while defined($para = <$fh>) && $para !~ m/^=head1 SYNOPSIS/; + die "$file does not contain a SYNOPSIS section" unless $para; + my @synop; + for ( 1..2 ) { # 1 for the usage, 2 for the description + my $para = <$fh>; + push @synop, $para; + } + close $fh; + PTDEBUG && _d("Raw SYNOPSIS text:", @synop); + my ($usage, $desc) = @synop; + die "The SYNOPSIS section in $file is not formatted properly" + unless $usage && $desc; + + $usage =~ s/^\s*Usage:\s+(.+)/$1/; + chomp $usage; + + $desc =~ s/\n/ /g; + $desc =~ s/\s{2,}/ /g; + $desc =~ s/\. ([A-Z][a-z])/. $1/g; + $desc =~ s/\s+$//; + + return ( + description => $desc, + usage => $usage, + ); +}; + +sub _d { + my ($package, undef, $line) = caller 0; + @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; } + map { defined $_ ? $_ : 'undef' } + @_; + print STDERR "# $package:$line $PID ", join(' ', @_), "\n"; +} + +if ( PTDEBUG ) { + print '# ', $^X, ' ', $], "\n"; + if ( my $uname = `uname -a` ) { + $uname =~ s/\s+/ /g; + print "# $uname\n"; + } + print '# Arguments: ', + join(' ', map { my $a = "_[$_]_"; $a =~ s/\n/\n# /g; $a; } @ARGV), "\n"; +} + +1; +} +# ########################################################################### +# End OptionParser package +# ########################################################################### + +# ########################################################################### +# Mo package +# This package is a copy without comments from the original. The original +# with comments and its test file can be found in the Bazaar repository at, +# lib/Mo.pm +# t/lib/Mo.t +# See https://launchpad.net/percona-toolkit for more information. +# ########################################################################### +{ +BEGIN { +$INC{"Mo.pm"} = __FILE__; +package Mo; +our $VERSION = '0.30_Percona'; # Forked from 0.30 of Mo. + +{ + no strict 'refs'; + sub _glob_for { + return \*{shift()} + } + + sub _stash_for { + return \%{ shift() . "::" }; + } +} + +use strict; +use warnings qw( FATAL all ); + +use Carp (); +use Scalar::Util qw(looks_like_number blessed); + + +our %TYPES = ( + Bool => sub { !$_[0] || (defined $_[0] && looks_like_number($_[0]) && $_[0] == 1) }, + Num => sub { defined $_[0] && looks_like_number($_[0]) }, + Int => sub { defined $_[0] && looks_like_number($_[0]) && $_[0] == int($_[0]) }, + Str => sub { defined $_[0] }, + Object => sub { defined $_[0] && blessed($_[0]) }, + FileHandle => sub { local $@; require IO::Handle; fileno($_[0]) && $_[0]->opened }, + + map { + my $type = /R/ ? $_ : uc $_; + $_ . "Ref" => sub { ref $_[0] eq $type } + } qw(Array Code Hash Regexp Glob Scalar) +); + +our %metadata_for; +{ + package Mo::Object; + + sub new { + my $class = shift; + my $args = $class->BUILDARGS(@_); + + my @args_to_delete; + while ( my ($attr, $meta) = each %{$metadata_for{$class}} ) { + next unless exists $meta->{init_arg}; + my $init_arg = $meta->{init_arg}; + + if ( defined $init_arg ) { + $args->{$attr} = delete $args->{$init_arg}; + } + else { + push @args_to_delete, $attr; + } + } + + delete $args->{$_} for @args_to_delete; + + for my $attribute ( keys %$args ) { + if ( my $coerce = $metadata_for{$class}{$attribute}{coerce} ) { + $args->{$attribute} = $coerce->($args->{$attribute}); + } + if ( my $I = $metadata_for{$class}{$attribute}{isa} ) { + ( (my $I_name), $I ) = @{$I}; + Mo::_check_type_constaints($attribute, $I, $I_name, $args->{$attribute}); + } + } + + while ( my ($attribute, $meta) = each %{$metadata_for{$class}} ) { + next unless $meta->{required}; + Carp::confess("Attribute ($attribute) is required for $class") + if ! exists $args->{$attribute} + } + + @_ = %$args; + my $self = bless $args, $class; + + my @build_subs; + my $linearized_isa = mro::get_linear_isa($class); + + for my $isa_class ( @$linearized_isa ) { + unshift @build_subs, *{ Mo::_glob_for "${isa_class}::BUILD" }{CODE}; + } + exists &$_ && $_->( $self, @_ ) for grep { defined } @build_subs; + return $self; + } + + sub BUILDARGS { + shift; + my $ref; + if ( @_ == 1 && ref($_[0]) ) { + Carp::confess("Single parameters to new() must be a HASH ref") + unless ref($_[0]) eq ref({}); + $ref = {%{$_[0]}} # We want a new reference, always + } + else { + $ref = { @_ }; + } + return $ref; + } +} + +my %export_for; +sub Mo::import { + warnings->import(qw(FATAL all)); + strict->import(); + + my $caller = scalar caller(); # Caller's package + my $caller_pkg = $caller . "::"; # Caller's package with :: at the end + my (%exports, %options); + + my (undef, @features) = @_; + my %ignore = ( map { $_ => 1 } qw( is isa init_arg builder buildargs clearer predicate build handles default required ) ); + for my $feature (grep { !$ignore{$_} } @features) { + { local $@; require "Mo/$feature.pm"; } + { + no strict 'refs'; + &{"Mo::${feature}::e"}( + $caller_pkg, + \%exports, + \%options, + \@_ + ); + } + } + + return if $exports{M}; + + %exports = ( + extends => sub { + for my $class ( map { "$_" } @_ ) { + $class =~ s{::|'}{/}g; + { local $@; eval { require "$class.pm" } } # or warn $@; + } + _set_package_isa($caller, @_); + _set_inherited_metadata($caller); + }, + has => sub { + my $names = shift; + for my $attribute ( ref $names ? @$names : $names ) { + my %args = @_; + my $method = ($args{is} || '') eq 'ro' + ? sub { + Carp::confess("Cannot assign a value to a read-only accessor at reader ${caller_pkg}${attribute}") + if $#_; + return $_[0]{$attribute}; + } + : sub { + return $#_ + ? $_[0]{$attribute} = $_[1] + : $_[0]{$attribute}; + }; + + $metadata_for{$caller}{$attribute} = (); + + if ( my $I = $args{isa} ) { + my $orig_I = $I; + my $type; + if ( $I =~ /\A(ArrayRef|Maybe)\[(.*)\]\z/ ) { + $I = _nested_constraints($attribute, $1, $2); + } + $metadata_for{$caller}{$attribute}{isa} = [$orig_I, $I]; + my $orig_method = $method; + $method = sub { + if ( $#_ ) { + Mo::_check_type_constaints($attribute, $I, $orig_I, $_[1]); + } + goto &$orig_method; + }; + } + + if ( my $builder = $args{builder} ) { + my $original_method = $method; + $method = sub { + $#_ + ? goto &$original_method + : ! exists $_[0]{$attribute} + ? $_[0]{$attribute} = $_[0]->$builder + : goto &$original_method + }; + } + + if ( my $code = $args{default} ) { + Carp::confess("${caller}::${attribute}'s default is $code, but should be a coderef") + unless ref($code) eq 'CODE'; + my $original_method = $method; + $method = sub { + $#_ + ? goto &$original_method + : ! exists $_[0]{$attribute} + ? $_[0]{$attribute} = $_[0]->$code + : goto &$original_method + }; + } + + if ( my $role = $args{does} ) { + my $original_method = $method; + $method = sub { + if ( $#_ ) { + Carp::confess(qq) + unless Scalar::Util::blessed($_[1]) && eval { $_[1]->does($role) } + } + goto &$original_method + }; + } + + if ( my $coercion = $args{coerce} ) { + $metadata_for{$caller}{$attribute}{coerce} = $coercion; + my $original_method = $method; + $method = sub { + if ( $#_ ) { + return $original_method->($_[0], $coercion->($_[1])) + } + goto &$original_method; + } + } + + $method = $options{$_}->($method, $attribute, @_) + for sort keys %options; + + *{ _glob_for "${caller}::$attribute" } = $method; + + if ( $args{required} ) { + $metadata_for{$caller}{$attribute}{required} = 1; + } + + if ($args{clearer}) { + *{ _glob_for "${caller}::$args{clearer}" } + = sub { delete shift->{$attribute} } + } + + if ($args{predicate}) { + *{ _glob_for "${caller}::$args{predicate}" } + = sub { exists shift->{$attribute} } + } + + if ($args{handles}) { + _has_handles($caller, $attribute, \%args); + } + + if (exists $args{init_arg}) { + $metadata_for{$caller}{$attribute}{init_arg} = $args{init_arg}; + } + } + }, + %exports, + ); + + $export_for{$caller} = [ keys %exports ]; + + for my $keyword ( keys %exports ) { + *{ _glob_for "${caller}::$keyword" } = $exports{$keyword} + } + *{ _glob_for "${caller}::extends" }{CODE}->( "Mo::Object" ) + unless @{ *{ _glob_for "${caller}::ISA" }{ARRAY} || [] }; +}; + +sub _check_type_constaints { + my ($attribute, $I, $I_name, $val) = @_; + ( ref($I) eq 'CODE' + ? $I->($val) + : (ref $val eq $I + || ($val && $val eq $I) + || (exists $TYPES{$I} && $TYPES{$I}->($val))) + ) + || Carp::confess( + qq + . qq + . (defined $val ? Mo::Dumper($val) : 'undef') ) +} + +sub _has_handles { + my ($caller, $attribute, $args) = @_; + my $handles = $args->{handles}; + + my $ref = ref $handles; + my $kv; + if ( $ref eq ref [] ) { + $kv = { map { $_,$_ } @{$handles} }; + } + elsif ( $ref eq ref {} ) { + $kv = $handles; + } + elsif ( $ref eq ref qr// ) { + Carp::confess("Cannot delegate methods based on a Regexp without a type constraint (isa)") + unless $args->{isa}; + my $target_class = $args->{isa}; + $kv = { + map { $_, $_ } + grep { $_ =~ $handles } + grep { !exists $Mo::Object::{$_} && $target_class->can($_) } + grep { $_ ne 'has' && $_ ne 'extends' } + keys %{ _stash_for $target_class } + }; + } + else { + Carp::confess("handles for $ref not yet implemented"); + } + + while ( my ($method, $target) = each %{$kv} ) { + my $name = _glob_for "${caller}::$method"; + Carp::confess("You cannot overwrite a locally defined method ($method) with a delegation") + if defined &$name; + + my ($target, @curried_args) = ref($target) ? @$target : $target; + *$name = sub { + my $self = shift; + my $delegate_to = $self->$attribute(); + my $error = "Cannot delegate $method to $target because the value of $attribute"; + Carp::confess("$error is not defined") unless $delegate_to; + Carp::confess("$error is not an object (got '$delegate_to')") + unless Scalar::Util::blessed($delegate_to) || (!ref($delegate_to) && $delegate_to->can($target)); + return $delegate_to->$target(@curried_args, @_); + } + } +} + +sub _nested_constraints { + my ($attribute, $aggregate_type, $type) = @_; + + my $inner_types; + if ( $type =~ /\A(ArrayRef|Maybe)\[(.*)\]\z/ ) { + $inner_types = _nested_constraints($1, $2); + } + else { + $inner_types = $TYPES{$type}; + } + + if ( $aggregate_type eq 'ArrayRef' ) { + return sub { + my ($val) = @_; + return unless ref($val) eq ref([]); + + if ($inner_types) { + for my $value ( @{$val} ) { + return unless $inner_types->($value) + } + } + else { + for my $value ( @{$val} ) { + return unless $value && ($value eq $type + || (Scalar::Util::blessed($value) && $value->isa($type))); + } + } + return 1; + }; + } + elsif ( $aggregate_type eq 'Maybe' ) { + return sub { + my ($value) = @_; + return 1 if ! defined($value); + if ($inner_types) { + return unless $inner_types->($value) + } + else { + return unless $value eq $type + || (Scalar::Util::blessed($value) && $value->isa($type)); + } + return 1; + } + } + else { + Carp::confess("Nested aggregate types are only implemented for ArrayRefs and Maybe"); + } +} + +sub _set_package_isa { + my ($package, @new_isa) = @_; + + *{ _glob_for "${package}::ISA" } = [@new_isa]; +} + +sub _set_inherited_metadata { + my $class = shift; + my $linearized_isa = mro::get_linear_isa($class); + my %new_metadata; + + for my $isa_class (reverse @$linearized_isa) { + %new_metadata = ( + %new_metadata, + %{ $metadata_for{$isa_class} || {} }, + ); + } + $metadata_for{$class} = \%new_metadata; +} + +sub unimport { + my $caller = scalar caller(); + my $stash = _stash_for( $caller ); + + delete $stash->{$_} for @{$export_for{$caller}}; +} + +sub Dumper { + require Data::Dumper; + local $Data::Dumper::Indent = 0; + local $Data::Dumper::Sortkeys = 0; + local $Data::Dumper::Quotekeys = 0; + local $Data::Dumper::Terse = 1; + + Data::Dumper::Dumper(@_) +} + +BEGIN { + if ($] >= 5.010) { + { local $@; require mro; } + } + else { + local $@; + eval { + require MRO::Compat; + } or do { + *mro::get_linear_isa = *mro::get_linear_isa_dfs = sub { + no strict 'refs'; + + my $classname = shift; + + my @lin = ($classname); + my %stored; + foreach my $parent (@{"$classname\::ISA"}) { + my $plin = mro::get_linear_isa_dfs($parent); + foreach (@$plin) { + next if exists $stored{$_}; + push(@lin, $_); + $stored{$_} = 1; + } + } + return \@lin; + }; + } + } +} + +} +1; +} +# ########################################################################### +# End Mo package +# ########################################################################### + +# ########################################################################### +# Cxn package +# This package is a copy without comments from the original. The original +# with comments and its test file can be found in the Bazaar repository at, +# lib/Cxn.pm +# t/lib/Cxn.t +# See https://launchpad.net/percona-toolkit for more information. +# ########################################################################### +{ +package Cxn; + +use strict; +use warnings FATAL => 'all'; +use English qw(-no_match_vars); +use Scalar::Util qw(blessed); +use constant { + PTDEBUG => $ENV{PTDEBUG} || 0, + PERCONA_TOOLKIT_TEST_USE_DSN_NAMES => $ENV{PERCONA_TOOLKIT_TEST_USE_DSN_NAMES} || 0, +}; + +sub new { + my ( $class, %args ) = @_; + my @required_args = qw(DSNParser OptionParser); + foreach my $arg ( @required_args ) { + die "I need a $arg argument" unless $args{$arg}; + }; + my ($dp, $o) = @args{@required_args}; + + my $dsn_defaults = $dp->parse_options($o); + my $prev_dsn = $args{prev_dsn}; + my $dsn = $args{dsn}; + if ( !$dsn ) { + $args{dsn_string} ||= 'h=' . ($dsn_defaults->{h} || 'localhost'); + + $dsn = $dp->parse( + $args{dsn_string}, $prev_dsn, $dsn_defaults); + } + elsif ( $prev_dsn ) { + $dsn = $dp->copy($prev_dsn, $dsn); + } + + my $self = { + dsn => $dsn, + dbh => $args{dbh}, + dsn_name => $dp->as_string($dsn, [qw(h P S)]), + hostname => '', + set => $args{set}, + NAME_lc => defined($args{NAME_lc}) ? $args{NAME_lc} : 1, + dbh_set => 0, + OptionParser => $o, + DSNParser => $dp, + is_cluster_node => undef, + }; + + return bless $self, $class; +} + +sub connect { + my ( $self ) = @_; + my $dsn = $self->{dsn}; + my $dp = $self->{DSNParser}; + my $o = $self->{OptionParser}; + + my $dbh = $self->{dbh}; + if ( !$dbh || !$dbh->ping() ) { + if ( $o->get('ask-pass') && !$self->{asked_for_pass} ) { + $dsn->{p} = OptionParser::prompt_noecho("Enter MySQL password: "); + $self->{asked_for_pass} = 1; + } + $dbh = $dp->get_dbh($dp->get_cxn_params($dsn), { AutoCommit => 1 }); + } + PTDEBUG && _d($dbh, 'Connected dbh to', $self->{name}); + + return $self->set_dbh($dbh); +} + +sub set_dbh { + my ($self, $dbh) = @_; + + if ( $self->{dbh} && $self->{dbh} == $dbh && $self->{dbh_set} ) { + PTDEBUG && _d($dbh, 'Already set dbh'); + return $dbh; + } + + PTDEBUG && _d($dbh, 'Setting dbh'); + + $dbh->{FetchHashKeyName} = 'NAME_lc' if $self->{NAME_lc}; + + my $sql = 'SELECT @@hostname, @@server_id'; + PTDEBUG && _d($dbh, $sql); + my ($hostname, $server_id) = $dbh->selectrow_array($sql); + PTDEBUG && _d($dbh, 'hostname:', $hostname, $server_id); + if ( $hostname ) { + $self->{hostname} = $hostname; + } + + if ( my $set = $self->{set}) { + $set->($dbh); + } + + $self->{dbh} = $dbh; + $self->{dbh_set} = 1; + return $dbh; +} + +sub dbh { + my ($self) = @_; + return $self->{dbh}; +} + +sub dsn { + my ($self) = @_; + return $self->{dsn}; +} + +sub name { + my ($self) = @_; + return $self->{dsn_name} if PERCONA_TOOLKIT_TEST_USE_DSN_NAMES; + return $self->{hostname} || $self->{dsn_name} || 'unknown host'; +} + +sub DESTROY { + my ($self) = @_; + if ( $self->{dbh} + && blessed($self->{dbh}) + && $self->{dbh}->can("disconnect") ) { + PTDEBUG && _d('Disconnecting dbh', $self->{dbh}, $self->{name}); + $self->{dbh}->disconnect(); + } + return; +} + +sub _d { + my ($package, undef, $line) = caller 0; + @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; } + map { defined $_ ? $_ : 'undef' } + @_; + print STDERR "# $package:$line $PID ", join(' ', @_), "\n"; +} + +1; +} +# ########################################################################### +# End Cxn package +# ########################################################################### + +# ########################################################################### +# Quoter package +# This package is a copy without comments from the original. The original +# with comments and its test file can be found in the Bazaar repository at, +# lib/Quoter.pm +# t/lib/Quoter.t +# See https://launchpad.net/percona-toolkit for more information. +# ########################################################################### +{ +package Quoter; + +use strict; +use warnings FATAL => 'all'; +use English qw(-no_match_vars); +use constant PTDEBUG => $ENV{PTDEBUG} || 0; + +sub new { + my ( $class, %args ) = @_; + return bless {}, $class; +} + +sub quote { + my ( $self, @vals ) = @_; + foreach my $val ( @vals ) { + $val =~ s/`/``/g; + } + return join('.', map { '`' . $_ . '`' } @vals); +} + +sub quote_val { + my ( $self, $val, %args ) = @_; + + return 'NULL' unless defined $val; # undef = NULL + return "''" if $val eq ''; # blank string = '' + return $val if $val =~ m/^0x[0-9a-fA-F]+$/ # quote hex data + && !$args{is_char}; # unless is_char is true + + $val =~ s/(['\\])/\\$1/g; + return "'$val'"; +} + +sub split_unquote { + my ( $self, $db_tbl, $default_db ) = @_; + my ( $db, $tbl ) = split(/[.]/, $db_tbl); + if ( !$tbl ) { + $tbl = $db; + $db = $default_db; + } + for ($db, $tbl) { + next unless $_; + s/\A`//; + s/`\z//; + s/``/`/g; + } + + return ($db, $tbl); +} + +sub literal_like { + my ( $self, $like ) = @_; + return unless $like; + $like =~ s/([%_])/\\$1/g; + return "'$like'"; +} + +sub join_quote { + my ( $self, $default_db, $db_tbl ) = @_; + return unless $db_tbl; + my ($db, $tbl) = split(/[.]/, $db_tbl); + if ( !$tbl ) { + $tbl = $db; + $db = $default_db; + } + $db = "`$db`" if $db && $db !~ m/^`/; + $tbl = "`$tbl`" if $tbl && $tbl !~ m/^`/; + return $db ? "$db.$tbl" : $tbl; +} + +sub serialize_list { + my ( $self, @args ) = @_; + return unless @args; + + return $args[0] if @args == 1 && !defined $args[0]; + + die "Cannot serialize multiple values with undef/NULL" + if grep { !defined $_ } @args; + + return join ',', map { quotemeta } @args; +} + +sub deserialize_list { + my ( $self, $string ) = @_; + return $string unless defined $string; + my @escaped_parts = $string =~ / + \G # Start of string, or end of previous match. + ( # Each of these is an element in the original list. + [^\\,]* # Anything not a backslash or a comma + (?: # When we get here, we found one of the above. + \\. # A backslash followed by something so we can continue + [^\\,]* # Same as above. + )* # Repeat zero of more times. + ) + , # Comma dividing elements + /sxgc; + + push @escaped_parts, pos($string) ? substr( $string, pos($string) ) : $string; + + my @unescaped_parts = map { + my $part = $_; + + my $char_class = utf8::is_utf8($part) # If it's a UTF-8 string, + ? qr/(?=\p{ASCII})\W/ # We only care about non-word + : qr/(?=\p{ASCII})\W|[\x{80}-\x{FF}]/; # Otherwise, + $part =~ s/\\($char_class)/$1/g; + $part; + } @escaped_parts; + + return @unescaped_parts; +} + +1; +} +# ########################################################################### +# End Quoter package +# ########################################################################### + +# ########################################################################### +# VersionParser package +# This package is a copy without comments from the original. The original +# with comments and its test file can be found in the Bazaar repository at, +# lib/VersionParser.pm +# t/lib/VersionParser.t +# See https://launchpad.net/percona-toolkit for more information. +# ########################################################################### +{ +package VersionParser; + +use Mo; +use Scalar::Util qw(blessed); +use English qw(-no_match_vars); +use constant PTDEBUG => $ENV{PTDEBUG} || 0; + +use overload ( + '""' => "version", + '<=>' => "cmp", + 'cmp' => "cmp", + fallback => 1, +); + +use Carp (); + +our $VERSION = 0.01; + +has major => ( + is => 'ro', + isa => 'Int', + required => 1, +); + +has [qw( minor revision )] => ( + is => 'ro', + isa => 'Num', +); + +has flavor => ( + is => 'ro', + isa => 'Str', + default => sub { 'Unknown' }, +); + +has innodb_version => ( + is => 'ro', + isa => 'Str', + default => sub { 'NO' }, +); + +sub series { + my $self = shift; + return $self->_join_version($self->major, $self->minor); +} + +sub version { + my $self = shift; + return $self->_join_version($self->major, $self->minor, $self->revision); +} + +sub is_in { + my ($self, $target) = @_; + + return $self eq $target; +} + +sub _join_version { + my ($self, @parts) = @_; + + return join ".", map { my $c = $_; $c =~ s/^0\./0/; $c } grep defined, @parts; +} +sub _split_version { + my ($self, $str) = @_; + my @version_parts = map { s/^0(?=\d)/0./; $_ } $str =~ m/(\d+)/g; + return @version_parts[0..2]; +} + +sub normalized_version { + my ( $self ) = @_; + my $result = sprintf('%d%02d%02d', map { $_ || 0 } $self->major, + $self->minor, + $self->revision); + PTDEBUG && _d($self->version, 'normalizes to', $result); + return $result; +} + +sub comment { + my ( $self, $cmd ) = @_; + my $v = $self->normalized_version(); + + return "/*!$v $cmd */" +} + +my @methods = qw(major minor revision); +sub cmp { + my ($left, $right) = @_; + my $right_obj = (blessed($right) && $right->isa(ref($left))) + ? $right + : ref($left)->new($right); + + my $retval = 0; + for my $m ( @methods ) { + last unless defined($left->$m) && defined($right_obj->$m); + $retval = $left->$m <=> $right_obj->$m; + last if $retval; + } + return $retval; +} + +sub BUILDARGS { + my $self = shift; + + if ( @_ == 1 ) { + my %args; + if ( blessed($_[0]) && $_[0]->can("selectrow_hashref") ) { + PTDEBUG && _d("VersionParser got a dbh, trying to get the version"); + my $dbh = $_[0]; + local $dbh->{FetchHashKeyName} = 'NAME_lc'; + my $query = eval { + $dbh->selectall_arrayref(q/SHOW VARIABLES LIKE 'version%'/, { Slice => {} }) + }; + if ( $query ) { + $query = { map { $_->{variable_name} => $_->{value} } @$query }; + @args{@methods} = $self->_split_version($query->{version}); + $args{flavor} = delete $query->{version_comment} + if $query->{version_comment}; + } + elsif ( eval { ($query) = $dbh->selectrow_array(q/SELECT VERSION()/) } ) { + @args{@methods} = $self->_split_version($query); + } + else { + Carp::confess("Couldn't get the version from the dbh while " + . "creating a VersionParser object: $@"); + } + $args{innodb_version} = eval { $self->_innodb_version($dbh) }; + } + elsif ( !ref($_[0]) ) { + @args{@methods} = $self->_split_version($_[0]); + } + + for my $method (@methods) { + delete $args{$method} unless defined $args{$method}; + } + @_ = %args if %args; + } + + return $self->SUPER::BUILDARGS(@_); +} + +sub _innodb_version { + my ( $self, $dbh ) = @_; + return unless $dbh; + my $innodb_version = "NO"; + + my ($innodb) = + grep { $_->{engine} =~ m/InnoDB/i } + map { + my %hash; + @hash{ map { lc $_ } keys %$_ } = values %$_; + \%hash; + } + @{ $dbh->selectall_arrayref("SHOW ENGINES", {Slice=>{}}) }; + if ( $innodb ) { + PTDEBUG && _d("InnoDB support:", $innodb->{support}); + if ( $innodb->{support} =~ m/YES|DEFAULT/i ) { + my $vars = $dbh->selectrow_hashref( + "SHOW VARIABLES LIKE 'innodb_version'"); + $innodb_version = !$vars ? "BUILTIN" + : ($vars->{Value} || $vars->{value}); + } + else { + $innodb_version = $innodb->{support}; # probably DISABLED or NO + } + } + + PTDEBUG && _d("InnoDB version:", $innodb_version); + return $innodb_version; +} + +sub _d { + my ($package, undef, $line) = caller 0; + @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; } + map { defined $_ ? $_ : 'undef' } + @_; + print STDERR "# $package:$line $PID ", join(' ', @_), "\n"; +} + +no Mo; +1; +} +# ########################################################################### +# End VersionParser package +# ########################################################################### + +# ########################################################################### +# Daemon package +# This package is a copy without comments from the original. The original +# with comments and its test file can be found in the Bazaar repository at, +# lib/Daemon.pm +# t/lib/Daemon.t +# See https://launchpad.net/percona-toolkit for more information. +# ########################################################################### +{ +package Daemon; + +use strict; +use warnings FATAL => 'all'; +use English qw(-no_match_vars); +use constant PTDEBUG => $ENV{PTDEBUG} || 0; + +use POSIX qw(setsid); + +sub new { + my ( $class, %args ) = @_; + foreach my $arg ( qw(o) ) { + die "I need a $arg argument" unless $args{$arg}; + } + my $o = $args{o}; + my $self = { + o => $o, + log_file => $o->has('log') ? $o->get('log') : undef, + PID_file => $o->has('pid') ? $o->get('pid') : undef, + }; + + check_PID_file(undef, $self->{PID_file}); + + PTDEBUG && _d('Daemonized child will log to', $self->{log_file}); + return bless $self, $class; +} + +sub daemonize { + my ( $self ) = @_; + + PTDEBUG && _d('About to fork and daemonize'); + defined (my $pid = fork()) or die "Cannot fork: $OS_ERROR"; + if ( $pid ) { + PTDEBUG && _d('Parent PID', $PID, 'exiting after forking child PID',$pid); + exit; + } + + PTDEBUG && _d('Daemonizing child PID', $PID); + $self->{PID_owner} = $PID; + $self->{child} = 1; + + POSIX::setsid() or die "Cannot start a new session: $OS_ERROR"; + chdir '/' or die "Cannot chdir to /: $OS_ERROR"; + + $self->_make_PID_file(); + + $OUTPUT_AUTOFLUSH = 1; + + PTDEBUG && _d('Redirecting STDIN to /dev/null'); + close STDIN; + open STDIN, '/dev/null' + or die "Cannot reopen STDIN to /dev/null: $OS_ERROR"; + + if ( $self->{log_file} ) { + PTDEBUG && _d('Redirecting STDOUT and STDERR to', $self->{log_file}); + close STDOUT; + open STDOUT, '>>', $self->{log_file} + or die "Cannot open log file $self->{log_file}: $OS_ERROR"; + + close STDERR; + open STDERR, ">&STDOUT" + or die "Cannot dupe STDERR to STDOUT: $OS_ERROR"; + } + else { + if ( -t STDOUT ) { + PTDEBUG && _d('No log file and STDOUT is a terminal;', + 'redirecting to /dev/null'); + close STDOUT; + open STDOUT, '>', '/dev/null' + or die "Cannot reopen STDOUT to /dev/null: $OS_ERROR"; + } + if ( -t STDERR ) { + PTDEBUG && _d('No log file and STDERR is a terminal;', + 'redirecting to /dev/null'); + close STDERR; + open STDERR, '>', '/dev/null' + or die "Cannot reopen STDERR to /dev/null: $OS_ERROR"; + } + } + + return; +} + +sub check_PID_file { + my ( $self, $file ) = @_; + my $PID_file = $self ? $self->{PID_file} : $file; + PTDEBUG && _d('Checking PID file', $PID_file); + if ( $PID_file && -f $PID_file ) { + my $pid; + eval { + chomp($pid = (slurp_file($PID_file) || '')); + }; + if ( $EVAL_ERROR ) { + die "The PID file $PID_file already exists but it cannot be read: " + . $EVAL_ERROR; + } + PTDEBUG && _d('PID file exists; it contains PID', $pid); + if ( $pid ) { + my $pid_is_alive = kill 0, $pid; + if ( $pid_is_alive ) { + die "The PID file $PID_file already exists " + . " and the PID that it contains, $pid, is running"; + } + else { + warn "Overwriting PID file $PID_file because the PID that it " + . "contains, $pid, is not running"; + } + } + else { + die "The PID file $PID_file already exists but it does not " + . "contain a PID"; + } + } + else { + PTDEBUG && _d('No PID file'); + } + return; +} + +sub make_PID_file { + my ( $self ) = @_; + if ( exists $self->{child} ) { + die "Do not call Daemon::make_PID_file() for daemonized scripts"; + } + $self->_make_PID_file(); + $self->{PID_owner} = $PID; + return; +} + +sub _make_PID_file { + my ( $self ) = @_; + + my $PID_file = $self->{PID_file}; + if ( !$PID_file ) { + PTDEBUG && _d('No PID file to create'); + return; + } + + $self->check_PID_file(); + + open my $PID_FH, '>', $PID_file + or die "Cannot open PID file $PID_file: $OS_ERROR"; + print $PID_FH $PID + or die "Cannot print to PID file $PID_file: $OS_ERROR"; + close $PID_FH + or die "Cannot close PID file $PID_file: $OS_ERROR"; + + PTDEBUG && _d('Created PID file:', $self->{PID_file}); + return; +} + +sub _remove_PID_file { + my ( $self ) = @_; + if ( $self->{PID_file} && -f $self->{PID_file} ) { + unlink $self->{PID_file} + or warn "Cannot remove PID file $self->{PID_file}: $OS_ERROR"; + PTDEBUG && _d('Removed PID file'); + } + else { + PTDEBUG && _d('No PID to remove'); + } + return; +} + +sub DESTROY { + my ( $self ) = @_; + + $self->_remove_PID_file() if ($self->{PID_owner} || 0) == $PID; + + return; +} + +sub slurp_file { + my ($file) = @_; + return unless $file; + open my $fh, "<", $file or die "Cannot open $file: $OS_ERROR"; + return do { local $/; <$fh> }; +} + +sub _d { + my ($package, undef, $line) = caller 0; + @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; } + map { defined $_ ? $_ : 'undef' } + @_; + print STDERR "# $package:$line $PID ", join(' ', @_), "\n"; +} + +1; +} +# ########################################################################### +# End Daemon package +# ########################################################################### + +# ########################################################################### +# Retry package +# This package is a copy without comments from the original. The original +# with comments and its test file can be found in the Bazaar repository at, +# lib/Retry.pm +# t/lib/Retry.t +# See https://launchpad.net/percona-toolkit for more information. +# ########################################################################### +{ +package Retry; + +use strict; +use warnings FATAL => 'all'; +use English qw(-no_match_vars); +use constant PTDEBUG => $ENV{PTDEBUG} || 0; + +sub new { + my ( $class, %args ) = @_; + my $self = { + %args, + }; + return bless $self, $class; +} + +sub retry { + my ( $self, %args ) = @_; + my @required_args = qw(try fail final_fail); + foreach my $arg ( @required_args ) { + die "I need a $arg argument" unless $args{$arg}; + }; + my ($try, $fail, $final_fail) = @args{@required_args}; + my $wait = $args{wait} || sub { sleep 1; }; + my $tries = $args{tries} || 3; + + my $last_error; + my $tryno = 0; + TRY: + while ( ++$tryno <= $tries ) { + PTDEBUG && _d("Try", $tryno, "of", $tries); + my $result; + eval { + $result = $try->(tryno=>$tryno); + }; + if ( $EVAL_ERROR ) { + PTDEBUG && _d("Try code failed:", $EVAL_ERROR); + $last_error = $EVAL_ERROR; + + if ( $tryno < $tries ) { # more retries + my $retry = $fail->(tryno=>$tryno, error=>$last_error); + last TRY unless $retry; + PTDEBUG && _d("Calling wait code"); + $wait->(tryno=>$tryno); + } + } + else { + PTDEBUG && _d("Try code succeeded"); + return $result; + } + } + + PTDEBUG && _d('Try code did not succeed'); + return $final_fail->(error=>$last_error); +} + +sub _d { + my ($package, undef, $line) = caller 0; + @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; } + map { defined $_ ? $_ : 'undef' } + @_; + print STDERR "# $package:$line $PID ", join(' ', @_), "\n"; +} + +1; +} +# ########################################################################### +# End Retry package +# ########################################################################### + +# ########################################################################### +# Transformers package +# This package is a copy without comments from the original. The original +# with comments and its test file can be found in the Bazaar repository at, +# lib/Transformers.pm +# t/lib/Transformers.t +# See https://launchpad.net/percona-toolkit for more information. +# ########################################################################### +{ +package Transformers; + +use strict; +use warnings FATAL => 'all'; +use English qw(-no_match_vars); +use constant PTDEBUG => $ENV{PTDEBUG} || 0; + +use Time::Local qw(timegm timelocal); +use Digest::MD5 qw(md5_hex); +use B qw(); + +require Exporter; +our @ISA = qw(Exporter); +our %EXPORT_TAGS = (); +our @EXPORT = (); +our @EXPORT_OK = qw( + micro_t + percentage_of + secs_to_time + time_to_secs + shorten + ts + parse_timestamp + unix_timestamp + any_unix_timestamp + make_checksum + crc32 + encode_json +); + +our $mysql_ts = qr/(\d\d)(\d\d)(\d\d) +(\d+):(\d+):(\d+)(\.\d+)?/; +our $proper_ts = qr/(\d\d\d\d)-(\d\d)-(\d\d)[T ](\d\d):(\d\d):(\d\d)(\.\d+)?/; +our $n_ts = qr/(\d{1,5})([shmd]?)/; # Limit \d{1,5} because \d{6} looks + +sub micro_t { + my ( $t, %args ) = @_; + my $p_ms = defined $args{p_ms} ? $args{p_ms} : 0; # precision for ms vals + my $p_s = defined $args{p_s} ? $args{p_s} : 0; # precision for s vals + my $f; + + $t = 0 if $t < 0; + + $t = sprintf('%.17f', $t) if $t =~ /e/; + + $t =~ s/\.(\d{1,6})\d*/\.$1/; + + if ($t > 0 && $t <= 0.000999) { + $f = ($t * 1000000) . 'us'; + } + elsif ($t >= 0.001000 && $t <= 0.999999) { + $f = sprintf("%.${p_ms}f", $t * 1000); + $f = ($f * 1) . 'ms'; # * 1 to remove insignificant zeros + } + elsif ($t >= 1) { + $f = sprintf("%.${p_s}f", $t); + $f = ($f * 1) . 's'; # * 1 to remove insignificant zeros + } + else { + $f = 0; # $t should = 0 at this point + } + + return $f; +} + +sub percentage_of { + my ( $is, $of, %args ) = @_; + my $p = $args{p} || 0; # float precision + my $fmt = $p ? "%.${p}f" : "%d"; + return sprintf $fmt, ($is * 100) / ($of ||= 1); +} + +sub secs_to_time { + my ( $secs, $fmt ) = @_; + $secs ||= 0; + return '00:00' unless $secs; + + $fmt ||= $secs >= 86_400 ? 'd' + : $secs >= 3_600 ? 'h' + : 'm'; + + return + $fmt eq 'd' ? sprintf( + "%d+%02d:%02d:%02d", + int($secs / 86_400), + int(($secs % 86_400) / 3_600), + int(($secs % 3_600) / 60), + $secs % 60) + : $fmt eq 'h' ? sprintf( + "%02d:%02d:%02d", + int(($secs % 86_400) / 3_600), + int(($secs % 3_600) / 60), + $secs % 60) + : sprintf( + "%02d:%02d", + int(($secs % 3_600) / 60), + $secs % 60); +} + +sub time_to_secs { + my ( $val, $default_suffix ) = @_; + die "I need a val argument" unless defined $val; + my $t = 0; + my ( $prefix, $num, $suffix ) = $val =~ m/([+-]?)(\d+)([a-z])?$/; + $suffix = $suffix || $default_suffix || 's'; + if ( $suffix =~ m/[smhd]/ ) { + $t = $suffix eq 's' ? $num * 1 # Seconds + : $suffix eq 'm' ? $num * 60 # Minutes + : $suffix eq 'h' ? $num * 3600 # Hours + : $num * 86400; # Days + + $t *= -1 if $prefix && $prefix eq '-'; + } + else { + die "Invalid suffix for $val: $suffix"; + } + return $t; +} + +sub shorten { + my ( $num, %args ) = @_; + my $p = defined $args{p} ? $args{p} : 2; # float precision + my $d = defined $args{d} ? $args{d} : 1_024; # divisor + my $n = 0; + my @units = ('', qw(k M G T P E Z Y)); + while ( $num >= $d && $n < @units - 1 ) { + $num /= $d; + ++$n; + } + return sprintf( + $num =~ m/\./ || $n + ? "%.${p}f%s" + : '%d', + $num, $units[$n]); +} + +sub ts { + my ( $time, $gmt ) = @_; + my ( $sec, $min, $hour, $mday, $mon, $year ) + = $gmt ? gmtime($time) : localtime($time); + $mon += 1; + $year += 1900; + my $val = sprintf("%d-%02d-%02dT%02d:%02d:%02d", + $year, $mon, $mday, $hour, $min, $sec); + if ( my ($us) = $time =~ m/(\.\d+)$/ ) { + $us = sprintf("%.6f", $us); + $us =~ s/^0\././; + $val .= $us; + } + return $val; +} + +sub parse_timestamp { + my ( $val ) = @_; + if ( my($y, $m, $d, $h, $i, $s, $f) + = $val =~ m/^$mysql_ts$/ ) + { + return sprintf "%d-%02d-%02d %02d:%02d:" + . (defined $f ? '%09.6f' : '%02d'), + $y + 2000, $m, $d, $h, $i, (defined $f ? $s + $f : $s); + } + return $val; +} + +sub unix_timestamp { + my ( $val, $gmt ) = @_; + if ( my($y, $m, $d, $h, $i, $s, $us) = $val =~ m/^$proper_ts$/ ) { + $val = $gmt + ? timegm($s, $i, $h, $d, $m - 1, $y) + : timelocal($s, $i, $h, $d, $m - 1, $y); + if ( defined $us ) { + $us = sprintf('%.6f', $us); + $us =~ s/^0\././; + $val .= $us; + } + } + return $val; +} + +sub any_unix_timestamp { + my ( $val, $callback ) = @_; + + if ( my ($n, $suffix) = $val =~ m/^$n_ts$/ ) { + $n = $suffix eq 's' ? $n # Seconds + : $suffix eq 'm' ? $n * 60 # Minutes + : $suffix eq 'h' ? $n * 3600 # Hours + : $suffix eq 'd' ? $n * 86400 # Days + : $n; # default: Seconds + PTDEBUG && _d('ts is now - N[shmd]:', $n); + return time - $n; + } + elsif ( $val =~ m/^\d{9,}/ ) { + PTDEBUG && _d('ts is already a unix timestamp'); + return $val; + } + elsif ( my ($ymd, $hms) = $val =~ m/^(\d{6})(?:\s+(\d+:\d+:\d+))?/ ) { + PTDEBUG && _d('ts is MySQL slow log timestamp'); + $val .= ' 00:00:00' unless $hms; + return unix_timestamp(parse_timestamp($val)); + } + elsif ( ($ymd, $hms) = $val =~ m/^(\d{4}-\d\d-\d\d)(?:[T ](\d+:\d+:\d+))?/) { + PTDEBUG && _d('ts is properly formatted timestamp'); + $val .= ' 00:00:00' unless $hms; + return unix_timestamp($val); + } + else { + PTDEBUG && _d('ts is MySQL expression'); + return $callback->($val) if $callback && ref $callback eq 'CODE'; + } + + PTDEBUG && _d('Unknown ts type:', $val); + return; +} + +sub make_checksum { + my ( $val ) = @_; + my $checksum = uc substr(md5_hex($val), -16); + PTDEBUG && _d($checksum, 'checksum for', $val); + return $checksum; +} + +sub crc32 { + my ( $string ) = @_; + return unless $string; + my $poly = 0xEDB88320; + my $crc = 0xFFFFFFFF; + foreach my $char ( split(//, $string) ) { + my $comp = ($crc ^ ord($char)) & 0xFF; + for ( 1 .. 8 ) { + $comp = $comp & 1 ? $poly ^ ($comp >> 1) : $comp >> 1; + } + $crc = (($crc >> 8) & 0x00FFFFFF) ^ $comp; + } + return $crc ^ 0xFFFFFFFF; +} + +my $got_json = eval { require JSON }; +sub encode_json { + return JSON::encode_json(@_) if $got_json; + my ( $data ) = @_; + return (object_to_json($data) || ''); +} + + +sub object_to_json { + my ($obj) = @_; + my $type = ref($obj); + + if($type eq 'HASH'){ + return hash_to_json($obj); + } + elsif($type eq 'ARRAY'){ + return array_to_json($obj); + } + else { + return value_to_json($obj); + } +} + +sub hash_to_json { + my ($obj) = @_; + my @res; + for my $k ( sort { $a cmp $b } keys %$obj ) { + push @res, string_to_json( $k ) + . ":" + . ( object_to_json( $obj->{$k} ) || value_to_json( $obj->{$k} ) ); + } + return '{' . ( @res ? join( ",", @res ) : '' ) . '}'; +} + +sub array_to_json { + my ($obj) = @_; + my @res; + + for my $v (@$obj) { + push @res, object_to_json($v) || value_to_json($v); + } + + return '[' . ( @res ? join( ",", @res ) : '' ) . ']'; +} + +sub value_to_json { + my ($value) = @_; + + return 'null' if(!defined $value); + + my $b_obj = B::svref_2object(\$value); # for round trip problem + my $flags = $b_obj->FLAGS; + return $value # as is + if $flags & ( B::SVp_IOK | B::SVp_NOK ) and !( $flags & B::SVp_POK ); # SvTYPE is IV or NV? + + my $type = ref($value); + + if( !$type ) { + return string_to_json($value); + } + else { + return 'null'; + } + +} + +my %esc = ( + "\n" => '\n', + "\r" => '\r', + "\t" => '\t', + "\f" => '\f', + "\b" => '\b', + "\"" => '\"', + "\\" => '\\\\', + "\'" => '\\\'', +); + +sub string_to_json { + my ($arg) = @_; + + $arg =~ s/([\x22\x5c\n\r\t\f\b])/$esc{$1}/g; + $arg =~ s/\//\\\//g; + $arg =~ s/([\x00-\x08\x0b\x0e-\x1f])/'\\u00' . unpack('H2', $1)/eg; + + utf8::upgrade($arg); + utf8::encode($arg); + + return '"' . $arg . '"'; +} + +sub _d { + my ($package, undef, $line) = caller 0; + @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; } + map { defined $_ ? $_ : 'undef' } + @_; + print STDERR "# $package:$line $PID ", join(' ', @_), "\n"; +} + +1; +} +# ########################################################################### +# End Transformers package +# ########################################################################### + +# ########################################################################### +# This is a combination of modules and programs in one -- a runnable module. +# http://www.perl.com/pub/a/2006/07/13/lightning-articles.html?page=last +# Or, look it up in the Camel book on pages 642 and 643 in the 3rd edition. +# +# Check at the end of this package for the call to main() which actually runs +# the program. +# ########################################################################### +package pt_agent; + +use strict; +use warnings FATAL => 'all'; +use English qw(-no_match_vars); + +use Percona::Toolkit; +use constant PTDEBUG => $ENV{PTDEBUG} || 0; + +use POSIX qw(signal_h); +use Time::HiRes qw(sleep time); +use Data::Dumper; +$Data::Dumper::Indent = 1; +$Data::Dumper::Sortkeys = 1; +$Data::Dumper::Quotekeys = 0; + +use sigtrap 'handler', \&sig_int, 'normal-signals'; + +my $oktorun = 1; + +sub main { + # Reset global vars else tests will fail in strange ways. + local @ARGV = @_; + $oktorun = 1; + + my $exit_status = 0; + + # ######################################################################## + # Get configuration information. + # ######################################################################## + my $o = new OptionParser(); + $o->get_specs(); + $o->get_opts(); + + my $dp = $o->DSNParser(); + $dp->prop('set-vars', $o->get('set-vars')); + + if ( !$o->get('help') ) { + } + + Pingback::validate_options($o); + + $o->usage_or_errors(); + + if ( $o->get('run-service') ) { + } + elsif ( $o->get('check-spool') ) { + } + else { + run_agent(); + } + + + PTDEBUG && _d('Exit status', $exit_status, + 'oktorun', $oktorun, + 'have time', $have_time->()); + return $exit_status; +} + +# ############################################################################ +# Subroutines +# ############################################################################ + +sub run_agent { + + # ######################################################################## + # If --pid, check it first since we'll die if it already exists. + # ######################################################################## + my $daemon; + if ( $o->get('pid') ) { + # We're not daemoninzing, it just handles PID stuff. Keep $daemon + # in the the scope of main() because when it's destroyed it automatically + # removes the PID file. + $daemon = new Daemon(o=>$o); + $daemon->make_PID_file(); + } +} + +sub run_service { +} + +sub check_spool { +} + +sub ts { + my ($msg) = @_; + my ($s, $m, $h, $d, $M) = localtime; + my $ts = sprintf('%02d-%02dT%02d:%02d:%02d', $M+1, $d, $h, $m, $s); + return $msg ? "$ts $msg" : $ts; +} + +# Catches signals so we can exit gracefully. +sub sig_int { + my ( $signal ) = @_; + if ( $oktorun ) { + print STDERR "# Caught SIG$signal.\n"; + $oktorun = 0; + } + else { + print STDERR "# Exiting on SIG$signal.\n"; + exit 1; + } +} + +sub _d { + my ($package, undef, $line) = caller 0; + @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; } + map { defined $_ ? $_ : 'undef' } + @_; + print STDERR "# $package:$line $PID ", join(' ', @_), "\n"; +} + +# ############################################################################ +# Run the program. +# ############################################################################ + +# https://bugs.launchpad.net/percona-toolkit/+bug/916999 +# http://www.mysqlperformanceblog.com/2012/02/21/dbd-mysql-4-014-breaks-pt-table-checksum-2-0/ +eval { + require DBD::mysql; +}; +if ( !$EVAL_ERROR && $DBD::mysql::VERSION eq "4.014" ) { + die "DBD::mysql v4.014 is installed, but it has as bug which causes " + . "pt-table-checksum to fail. Please upgrade DBD::mysql to any " + . "newer version.\n" +} + +if ( !caller ) { exit main(@ARGV); } + +1; # Because this is a module as well as a script. + +# ############################################################################ +# Documentation +# ############################################################################ +=pod + +=head1 NAME + +pt-agent - Implement Percona Web Services + +=head1 SYNOPSIS + +Usage: pt-agent [OPTIONS] + +pt-agent implements Percona Web Services. + +=head1 RISKS + +The following section is included to inform users about the potential risks, +whether known or unknown, of using this tool. The two main categories of risks +are those created by the nature of the tool (e.g. read-only tools vs. read-write +tools) and those created by bugs. + +TODO + +The authoritative source for updated information is always the online issue +tracking system. Issues that affect this tool will be marked as such. You can +see a list of such issues at the following URL: +L. + +See also L<"LIMITATIONS"> and L<"BUGS">. + +=head1 DESCRIPTION + +pt-agent implement Percona Web Services. +TODO + +=head1 EXIT STATUS + +TODO + +=head1 OPTIONS + +L<"--check-spool"> and L<"--run-service"> are mutually exclusive. + +=over + +=item --ask-pass + +group: Connection + +Prompt for a password when connecting to MySQL. + +=item --check-interval + +type: time; default: 1; group: Throttle + +Sleep time between checks for L<"--max-lag">. + +=item --check-spool + +Check for new L<"--spool"> data to send. + +=item --config + +type: Array; group: Config + +Read this comma-separated list of config files; if specified, this must be the +first option on the command line. + +See the L<"--help"> output for a list of default config files. + +=item --defaults-file + +short form: -F; type: string; group: Connection + +Only read mysql options from the given file. You must give an absolute +pathname. + +=item --help + +group: Help + +Show help and exit. + +=item --host + +short form: -h; type: string; default: localhost; group: Connection + +Host to connect to. + +=item --lib + +type: string; default: /var/lib/pt-agent + +Data dir. + +=item --log + +type: string; default: /var/log/pt-agent.log + +=item --password + +short form: -p; type: string; group: Connection + +Password to use when connecting. + +=item --pid + +type: string; default: /var/run/pt-agent.pid + +Create the given PID file. The file contains the process ID of the script. +The PID file is removed when the script exits. Before starting, the script +checks if the PID file already exists. If it does not, then the script creates +and writes its own PID to it. If it does, then the script checks the following: +if the file contains a PID and a process is running with that PID, then +the script dies; or, if there is no process running with that PID, then the +script overwrites the file with its own PID and starts; else, if the file +contains no PID, then the script dies. + +=item --port + +short form: -P; type: int; group: Connection + +Port number to use for connection. + +=item --quiet + +short form: -q; cumulative: yes; default: 0 + +Print only the most important information (disables L<"--progress">). +Specifying this option once causes the tool to print only errors, warnings, and +tables that have checksum differences. + +=item --retries + +type: int; default: 2 + +Retry a chunk this many times when there is a nonfatal error. Nonfatal errors +are problems such as a lock wait timeout or the query being killed. + +=item --run-service + +Run a Percona Web Service. + +=item --set-vars + +type: string; default: wait_timeout=10000; group: Connection + +Set these MySQL variables. Immediately after connecting to MySQL, this +string will be appended to SET and executed. + +=item --socket + +short form: -S; type: string; group: Connection + +Socket file to use for connection. + +=item --spool + +type: string; default: /var/spool/pt-agent + +Spool directory. + +=item --spool-interval + +type: time; default: 10m + +How often to send spool data to Percona. + +=item --user + +short form: -u; type: string; group: Connection + +User for login if not current user. + +=item --version + +group: Help + +Show version and exit. + +=item --version-check + +type: string; default: auto + +Send program versions to Percona and print suggested upgrades and problems. +Possible values for --version-check: + +=for comment ignore-pt-internal-value +MAGIC_version_check + +https, http, auto, off + +C first tries using C, and resorts to C if that fails. +Keep in mind that C might not be available if +C is not installed on your system, although +C<--version-check http> should work everywhere. + +The version check feature causes the tool to send and receive data from +Percona over the web. The data contains program versions from the local +machine. Percona uses the data to focus development on the most widely +used versions of programs, and to suggest to customers possible upgrades +and known bad versions of programs. + +For more information, visit L. + +=back + +=head1 DSN OPTIONS + +These DSN options are used to create a DSN. Each option is given like +C. The options are case-sensitive, so P and p are not the +same option. There cannot be whitespace before or after the C<=> and +if the value contains whitespace it must be quoted. DSN options are +comma-separated. See the L manpage for full details. + +=over + +=item * A + +dsn: charset; copy: yes + +Default character set. + +=item * D + +copy: no + +DSN table database. + +=item * F + +dsn: mysql_read_default_file; copy: yes + +Defaults file for connection values. + +=item * h + +dsn: host; copy: yes + +Connect to host. + +=item * p + +dsn: password; copy: yes + +Password to use when connecting. + +=item * P + +dsn: port; copy: yes + +Port number to use for connection. + +=item * S + +dsn: mysql_socket; copy: no + +Socket file to use for connection. + +=item * t + +copy: no + +DSN table table. + +=item * u + +dsn: user; copy: yes + +User for login if not current user. + +=back + +=head1 ENVIRONMENT + +The environment variable C enables verbose debugging output to STDERR. +To enable debugging and capture all output to a file, run the tool like: + + PTDEBUG=1 pt-agent ... > FILE 2>&1 + +Be careful: debugging output is voluminous and can generate several megabytes +of output. + +=head1 SYSTEM REQUIREMENTS + +You need Perl, DBI, DBD::mysql, and some core packages that ought to be +installed in any reasonably new version of Perl. + +=head1 LIMITATIONS + +=over + +=item Replicas using row-based replication + +pt-agent requires statement-based replication, and it sets +C on the master, but due to a MySQL limitation +replicas do not honor this change. Therefore, checksums will not replicate +past any replicas using row-based replication that are masters for +further replicas. + +The tool automatically checks the C on all servers. +See L<"--[no]check-binlog-format"> . + +(L) + +=back + +=head1 BUGS + +For a list of known bugs, see L. + +Please report bugs at L. +Include the following information in your bug report: + +=over + +=item * Complete command-line used to run the tool + +=item * Tool L<"--version"> + +=item * MySQL version of all servers involved + +=item * Output from the tool including STDERR + +=item * Input files (log/dump/config files, etc.) + +=back + +If possible, include debugging output by running the tool with C; +see L<"ENVIRONMENT">. + +=head1 DOWNLOADING + +Visit L to download the +latest release of Percona Toolkit. Or, get the latest release from the +command line: + + wget percona.com/get/percona-toolkit.tar.gz + + wget percona.com/get/percona-toolkit.rpm + + wget percona.com/get/percona-toolkit.deb + +You can also get individual tools from the latest release: + + wget percona.com/get/TOOL + +Replace C with the name of any tool. + +=head1 AUTHORS + +Baron Schwartz and Daniel Nichter + +=head1 ACKNOWLEDGMENTS + +Claus Jeppesen, Francois Saint-Jacques, Giuseppe Maxia, Heikki Tuuri, +James Briggs, Martin Friebe, and Sergey Zhuravlev + +=head1 ABOUT PERCONA TOOLKIT + +This tool is part of Percona Toolkit, a collection of advanced command-line +tools developed by Percona for MySQL support and consulting. Percona Toolkit +was forked from two projects in June, 2011: Maatkit and Aspersa. Those +projects were created by Baron Schwartz and developed primarily by him and +Daniel Nichter, both of whom are employed by Percona. Visit +L for more software developed by Percona. + +=head1 COPYRIGHT, LICENSE, AND WARRANTY + +This program is copyright 2012 Percona Inc. +Feedback and improvements are welcome. + +THIS PROGRAM IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, version 2; OR the Perl Artistic License. On UNIX and similar +systems, you can issue `man perlgpl' or `man perlartistic' to read these +licenses. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 59 Temple +Place, Suite 330, Boston, MA 02111-1307 USA. + +=head1 VERSION + +pt-agent 3.0.0 + +=cut diff --git a/lib/Mo.pm b/lib/Mo.pm deleted file mode 100644 index ba96a512..00000000 --- a/lib/Mo.pm +++ /dev/null @@ -1,519 +0,0 @@ -# This program is copyright 2007-2011 Baron Schwartz, 2012 Percona Inc. -# Feedback and improvements are welcome. -# -# THIS PROGRAM IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED -# WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF -# MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free Software -# Foundation, version 2; OR the Perl Artistic License. On UNIX and similar -# systems, you can issue `man perlgpl' or `man perlartistic' to read these -# licenses. -# -# You should have received a copy of the GNU General Public License along with -# this program; if not, write to the Free Software Foundation, Inc., 59 Temple -# Place, Suite 330, Boston, MA 02111-1307 USA. -# ########################################################################### -# Mo package -# ########################################################################### -# Package: Mo -# Mo provides a miniature object system in the style of Moose and Moo. -BEGIN { -$INC{"Mo.pm"} = __FILE__; -package Mo; -our $VERSION = '0.30_Percona'; # Forked from 0.30 of Mo. - -{ - # Gets the glob from a given string. - no strict 'refs'; - sub _glob_for { - return \*{shift()} - } - - # Gets the stash from a given string. A larger explanation about hashes in Mo::Percona - sub _stash_for { - return \%{ shift() . "::" }; - } -} - -use strict; -use warnings qw( FATAL all ); - -use Carp (); -use Scalar::Util qw(looks_like_number blessed); - -# Basic types for isa. If you want a new type, either add it here, -# or give isa a coderef. - -our %TYPES = ( - Bool => sub { !$_[0] || (defined $_[0] && looks_like_number($_[0]) && $_[0] == 1) }, - Num => sub { defined $_[0] && looks_like_number($_[0]) }, - Int => sub { defined $_[0] && looks_like_number($_[0]) && $_[0] == int($_[0]) }, - Str => sub { defined $_[0] }, - Object => sub { defined $_[0] && blessed($_[0]) }, - FileHandle => sub { local $@; require IO::Handle; fileno($_[0]) && $_[0]->opened }, - - map { - my $type = /R/ ? $_ : uc $_; - $_ . "Ref" => sub { ref $_[0] eq $type } - } qw(Array Code Hash Regexp Glob Scalar) -); - -our %metadata_for; -{ - # Mo::Object is the parent of every Mo-derived object. Here's where new - # and BUILDARGS gets inherited from. - package Mo::Object; - - sub new { - my $class = shift; - my $args = $class->BUILDARGS(@_); - - my @args_to_delete; - while ( my ($attr, $meta) = each %{$metadata_for{$class}} ) { - next unless exists $meta->{init_arg}; - my $init_arg = $meta->{init_arg}; - - # If init_arg is defined, then we - if ( defined $init_arg ) { - $args->{$attr} = delete $args->{$init_arg}; - } - else { - push @args_to_delete, $attr; - } - } - - delete $args->{$_} for @args_to_delete; - - for my $attribute ( keys %$args ) { - # coerce - if ( my $coerce = $metadata_for{$class}{$attribute}{coerce} ) { - $args->{$attribute} = $coerce->($args->{$attribute}); - } - # isa - if ( my $I = $metadata_for{$class}{$attribute}{isa} ) { - ( (my $I_name), $I ) = @{$I}; - Mo::_check_type_constaints($attribute, $I, $I_name, $args->{$attribute}); - } - } - - while ( my ($attribute, $meta) = each %{$metadata_for{$class}} ) { - next unless $meta->{required}; - Carp::confess("Attribute ($attribute) is required for $class") - if ! exists $args->{$attribute} - } - - @_ = %$args; - my $self = bless $args, $class; - - # BUILD - my @build_subs; - my $linearized_isa = mro::get_linear_isa($class); - - for my $isa_class ( @$linearized_isa ) { - unshift @build_subs, *{ Mo::_glob_for "${isa_class}::BUILD" }{CODE}; - } - # If &class::BUILD exists, for every class in - # the linearized ISA, call it. - # XXX I _think_ that this uses exists correctly, since - # a class could define a stub for BUILD and then AUTOLOAD - # the body. Should check what Moose does. - exists &$_ && $_->( $self, @_ ) for grep { defined } @build_subs; - return $self; - } - - # Base BUILDARGS. - sub BUILDARGS { - shift; - my $ref; - if ( @_ == 1 && ref($_[0]) ) { - Carp::confess("Single parameters to new() must be a HASH ref") - unless ref($_[0]) eq ref({}); - $ref = {%{$_[0]}} # We want a new reference, always - } - else { - $ref = { @_ }; - } - return $ref; - } -} - -my %export_for; -sub Mo::import { - # Set warnings and strict for the caller. - warnings->import(qw(FATAL all)); - strict->import(); - - my $caller = scalar caller(); # Caller's package - my $caller_pkg = $caller . "::"; # Caller's package with :: at the end - my (%exports, %options); - - # Load each feature and call its &e. - my (undef, @features) = @_; - my %ignore = ( map { $_ => 1 } qw( is isa init_arg builder buildargs clearer predicate build handles default required ) ); - for my $feature (grep { !$ignore{$_} } @features) { - { local $@; require "Mo/$feature.pm"; } - { - no strict 'refs'; - &{"Mo::${feature}::e"}( - $caller_pkg, - \%exports, - \%options, - \@_ - ); - } - } - - return if $exports{M}; - - %exports = ( - extends => sub { - for my $class ( map { "$_" } @_ ) { - # Try loading the class, but don't croak if we fail. - $class =~ s{::|'}{/}g; - { local $@; eval { require "$class.pm" } } # or warn $@; - } - _set_package_isa($caller, @_); - _set_inherited_metadata($caller); - }, - has => sub { - my $names = shift; - for my $attribute ( ref $names ? @$names : $names ) { - my %args = @_; - my $method = ($args{is} || '') eq 'ro' - ? sub { - Carp::confess("Cannot assign a value to a read-only accessor at reader ${caller_pkg}${attribute}") - if $#_; - return $_[0]{$attribute}; - } - : sub { - return $#_ - ? $_[0]{$attribute} = $_[1] - : $_[0]{$attribute}; - }; - - $metadata_for{$caller}{$attribute} = (); - - # isa => Constaint, - if ( my $I = $args{isa} ) { - my $orig_I = $I; - my $type; - if ( $I =~ /\A(ArrayRef|Maybe)\[(.*)\]\z/ ) { - $I = _nested_constraints($attribute, $1, $2); - } - $metadata_for{$caller}{$attribute}{isa} = [$orig_I, $I]; - my $orig_method = $method; - $method = sub { - if ( $#_ ) { - Mo::_check_type_constaints($attribute, $I, $orig_I, $_[1]); - } - goto &$orig_method; - }; - } - - # XXX TODO: Inline builder and default into the actual method, for speed. - # builder => '_builder_method', - if ( my $builder = $args{builder} ) { - my $original_method = $method; - $method = sub { - $#_ - ? goto &$original_method - : ! exists $_[0]{$attribute} - ? $_[0]{$attribute} = $_[0]->$builder - : goto &$original_method - }; - } - - # default => CodeRef, - if ( my $code = $args{default} ) { - Carp::confess("${caller}::${attribute}'s default is $code, but should be a coderef") - unless ref($code) eq 'CODE'; - my $original_method = $method; - $method = sub { - $#_ - ? goto &$original_method - : ! exists $_[0]{$attribute} - ? $_[0]{$attribute} = $_[0]->$code - : goto &$original_method - }; - } - - # does => 'Role', - if ( my $role = $args{does} ) { - my $original_method = $method; - $method = sub { - if ( $#_ ) { - Carp::confess(qq) - unless Scalar::Util::blessed($_[1]) && eval { $_[1]->does($role) } - } - goto &$original_method - }; - } - - # coerce => CodeRef, - if ( my $coercion = $args{coerce} ) { - $metadata_for{$caller}{$attribute}{coerce} = $coercion; - my $original_method = $method; - $method = sub { - if ( $#_ ) { - return $original_method->($_[0], $coercion->($_[1])) - } - goto &$original_method; - } - } - - # Call the extra features; that is, things loaded from - # the Mo::etc namespace, and not implemented here. - $method = $options{$_}->($method, $attribute, @_) - for sort keys %options; - - # Actually put the attribute's accessor in the class - *{ _glob_for "${caller}::$attribute" } = $method; - - if ( $args{required} ) { - $metadata_for{$caller}{$attribute}{required} = 1; - } - - if ($args{clearer}) { - *{ _glob_for "${caller}::$args{clearer}" } - = sub { delete shift->{$attribute} } - } - - if ($args{predicate}) { - *{ _glob_for "${caller}::$args{predicate}" } - = sub { exists shift->{$attribute} } - } - - if ($args{handles}) { - _has_handles($caller, $attribute, \%args); - } - - if (exists $args{init_arg}) { - $metadata_for{$caller}{$attribute}{init_arg} = $args{init_arg}; - } - } - }, - %exports, - ); - - # We keep this so code doing 'no Mo;' actually does a cleanup. - $export_for{$caller} = [ keys %exports ]; - - # Export has, extends and sosuch. - for my $keyword ( keys %exports ) { - *{ _glob_for "${caller}::$keyword" } = $exports{$keyword} - } - # Set up our caller's ISA, unless they already set it manually themselves, - # in which case we assume they know what they are doing. - # XXX weird syntax here because we want to call the classes' extends at - # least once, to avoid warnings. - *{ _glob_for "${caller}::extends" }{CODE}->( "Mo::Object" ) - unless @{ *{ _glob_for "${caller}::ISA" }{ARRAY} || [] }; -}; - -sub _check_type_constaints { - my ($attribute, $I, $I_name, $val) = @_; - ( ref($I) eq 'CODE' - ? $I->($val) - : (ref $val eq $I - || ($val && $val eq $I) - || (exists $TYPES{$I} && $TYPES{$I}->($val))) - ) - || Carp::confess( - qq - . qq - . (defined $val ? Mo::Dumper($val) : 'undef') ) -} - -# handles handles -sub _has_handles { - my ($caller, $attribute, $args) = @_; - my $handles = $args->{handles}; - - my $ref = ref $handles; - my $kv; - if ( $ref eq ref [] ) { - # handles => [ ... list of methods ... ], - $kv = { map { $_,$_ } @{$handles} }; - } - elsif ( $ref eq ref {} ) { - # handles => { 'method_to_install' => 'original_method' | [ 'original_method', ... curried arguments ... ], }, - $kv = $handles; - } - elsif ( $ref eq ref qr// ) { - # handles => qr/PAT/, - Carp::confess("Cannot delegate methods based on a Regexp without a type constraint (isa)") - unless $args->{isa}; - my $target_class = $args->{isa}; - $kv = { - map { $_, $_ } - grep { $_ =~ $handles } - grep { !exists $Mo::Object::{$_} && $target_class->can($_) } - grep { $_ ne 'has' && $_ ne 'extends' } - keys %{ _stash_for $target_class } - }; - } - else { - Carp::confess("handles for $ref not yet implemented"); - } - - while ( my ($method, $target) = each %{$kv} ) { - my $name = _glob_for "${caller}::$method"; - Carp::confess("You cannot overwrite a locally defined method ($method) with a delegation") - if defined &$name; - - # If we have an arrayref, they are currying some arguments. - my ($target, @curried_args) = ref($target) ? @$target : $target; - *$name = sub { - my $self = shift; - my $delegate_to = $self->$attribute(); - my $error = "Cannot delegate $method to $target because the value of $attribute"; - Carp::confess("$error is not defined") unless $delegate_to; - Carp::confess("$error is not an object (got '$delegate_to')") - unless Scalar::Util::blessed($delegate_to) || (!ref($delegate_to) && $delegate_to->can($target)); - return $delegate_to->$target(@curried_args, @_); - } - } -} - -# Nested (or parametized) constraints look like this: ArrayRef[CONSTRAINT] or -# Maybe[CONSTRAINT]. This function returns a coderef that implements one of -# these. -sub _nested_constraints { - my ($attribute, $aggregate_type, $type) = @_; - - my $inner_types; - if ( $type =~ /\A(ArrayRef|Maybe)\[(.*)\]\z/ ) { - # If the inner constraint -- the part within brackets -- is also a parametized - # constraint, then call this function recursively. - $inner_types = _nested_constraints($1, $2); - } - else { - # Otherwise, try checking if it's one of the built-in types. - $inner_types = $TYPES{$type}; - } - - if ( $aggregate_type eq 'ArrayRef' ) { - return sub { - my ($val) = @_; - return unless ref($val) eq ref([]); - - if ($inner_types) { - for my $value ( @{$val} ) { - return unless $inner_types->($value) - } - } - else { - # $inner_types isn't set, we are dealing with a class name. - for my $value ( @{$val} ) { - return unless $value && ($value eq $type - || (Scalar::Util::blessed($value) && $value->isa($type))); - } - } - return 1; - }; - } - elsif ( $aggregate_type eq 'Maybe' ) { - return sub { - my ($value) = @_; - # For Maybe, undef is valid - return 1 if ! defined($value); - # Otherwise, defer to the inner type - if ($inner_types) { - return unless $inner_types->($value) - } - else { - return unless $value eq $type - || (Scalar::Util::blessed($value) && $value->isa($type)); - } - return 1; - } - } - else { - Carp::confess("Nested aggregate types are only implemented for ArrayRefs and Maybe"); - } -} - -# Sets a package's @ISA to the list passed in. Overwrites any previous values. -sub _set_package_isa { - my ($package, @new_isa) = @_; - - *{ _glob_for "${package}::ISA" } = [@new_isa]; -} - -# Each class has its own metadata. When a class inhyerits attributes, -# it should also inherit the attribute metadata. -sub _set_inherited_metadata { - my $class = shift; - my $linearized_isa = mro::get_linear_isa($class); - my %new_metadata; - - # Walk @ISA in reverse, grabbing the metadata for each - # class. Attributes with the same name defined in more - # specific classes override their parent's attributes. - for my $isa_class (reverse @$linearized_isa) { - %new_metadata = ( - %new_metadata, - %{ $metadata_for{$isa_class} || {} }, - ); - } - $metadata_for{$class} = \%new_metadata; -} - -sub unimport { - my $caller = scalar caller(); - my $stash = _stash_for( $caller ); - - delete $stash->{$_} for @{$export_for{$caller}}; -} - -sub Dumper { - require Data::Dumper; - local $Data::Dumper::Indent = 0; - local $Data::Dumper::Sortkeys = 0; - local $Data::Dumper::Quotekeys = 0; - local $Data::Dumper::Terse = 1; - - Data::Dumper::Dumper(@_) -} - -BEGIN { - # mro is the method resolution order. The module itself is core in - # recent Perls; In older Perls it's available from MRO::Compat from - # CPAN, and in case that isn't available to us, we inline the barest - # funcionality. - if ($] >= 5.010) { - { local $@; require mro; } - } - else { - local $@; - eval { - require MRO::Compat; - } or do { - *mro::get_linear_isa = *mro::get_linear_isa_dfs = sub { - no strict 'refs'; - - my $classname = shift; - - my @lin = ($classname); - my %stored; - foreach my $parent (@{"$classname\::ISA"}) { - my $plin = mro::get_linear_isa_dfs($parent); - foreach (@$plin) { - next if exists $stored{$_}; - push(@lin, $_); - $stored{$_} = 1; - } - } - return \@lin; - }; - } - } -} - -} -1; -# ########################################################################### -# End Mo package -# ########################################################################### diff --git a/lib/HTTPMicro.pm b/lib/Percona/HTTP/Mirco.pm similarity index 100% rename from lib/HTTPMicro.pm rename to lib/Percona/HTTP/Mirco.pm diff --git a/lib/Percona/WebAPI/Client.pm b/lib/Percona/WebAPI/Client.pm new file mode 100644 index 00000000..68e0def8 --- /dev/null +++ b/lib/Percona/WebAPI/Client.pm @@ -0,0 +1,301 @@ +package Percona::WebAPI::Client; + +our $VERSION = '0.01'; + +use LWP; +use JSON; +use Scalar::Util qw(blessed); +use English qw(-no_match_vars); + +use Percona::Toolkit; + +has 'api_key' => ( + is => 'ro', + isa => 'Str', + required => 1, +); + +has 'base_url' => ( + is => 'rw', + isa => 'Str', + default => 'https://api.tools.percona.com', + required => 1, +); + +has 'links' => ( + is => 'rw', + isa => 'HashRef', + lazy => 1, + default => sub { return +{} }, +); + +has 'ua' => ( + is => 'rw', + isa => 'LWP::UserAgent', + lazy => 1, + required => 1, + builder => '_build_ua', +); + +has 'response' => ( + is => 'rw', + isa => 'Object', +); + +sub _build_ua { + my $self = shift; + my $ua = LWP::UserAgent->new; + $ua->agent("Percona::WebAPI::Client/$Percona::WebAPI::Client::VERSION"); + $ua->default_header('application/json'); + $ua->default_header('X-Percona-API-Key', $self->api_key); + return $ua; +} + +sub BUILD { + my ($self) = @_; + + eval { + $self->_request( + method => 'GET', + url => $self->base_url, + ); + }; + if ( my $e = $EVAL_ERROR ) { + if (blessed($e) && $e->isa('Percona::WebAPI::Exception::Request')) { + die $e; + } + else { + die "Unknown error: $e"; + } + } + + my $entry_links = decode_json($self->response->content); + PTDEBUG && _d('Entry links', $entry_links); + + $self->links($entry_links); + + return; +} + +sub get { + my ($self, %args) = @_; + + # Arguments: + my @required_args = ( + 'link', # A resource link (e.g. $run->links->{results}) + ); + my ($link) = @args{@required_args}; + + # Returns: + my @resources; # Resources from the requested link + + # Get resource representations from the link. The server should always + # return a list of resource reps, even if there's only one resource. + eval { + $self->_request( + method => 'GET', + url => $link, + ); + }; + if ( my $e = $EVAL_ERROR ) { + if (blessed($e) && $e->isa('Percona::WebAPI::Exception::Request')) { + die $e; + } + else { + die "Unknown error: $e"; + } + } + + # Transform the resource representations into an arrayref of hashrefs. + # Each hashref contains (hopefully) all the attribs necessary to create + # a corresponding resource object. + my $res; + eval { + $res = decode_json($self->response->content); + }; + if ( $EVAL_ERROR ) { + warn sprintf "Error decoding resource: %s: %s", + $self->response->content, + $EVAL_ERROR; + return; + } + + my $objs; + my $res_type = $self->response->headers->{'x-percona-webapi-content-type'}; + if ( $res_type ) { + eval { + my $type = "Percona::WebAPI::Resource::$res_type"; + + # Create resource objects using the server-provided attribs. + if ( ref $res->{content} eq 'ARRAY' ) { + PTDEBUG && _d('Got a list of', $res_type, 'resources'); + foreach my $attribs ( @{$res->{content}} ) { + my $obj = $type->new(%$attribs); + push @$objs, $obj; + } + } + else { + PTDEBUG && _d('Got a', $res_type, 'resource'); + $objs = $type->new(%{$res->{content}}); + } + }; + if ( $EVAL_ERROR ) { + warn "Error creating $res_type resource objects: $EVAL_ERROR"; + return; + } + } + + $self->update_links($res->{links}); + + return $objs; +} + +sub post { + my $self = shift; + return $self->_set( + @_, + method => 'POST', + ); +} + +sub delete { + my ($self, %args) = @_; + + # Arguments: + my @required_args = ( + 'link', # A resource link (e.g. $run->links->{results}) + ); + my ($link) = @args{@required_args}; + + eval { + $self->_request( + method => 'DELETE', + url => $link, + headers => { 'Content-Length' => 0 }, + ); + }; + if ( my $e = $EVAL_ERROR ) { + if (blessed($e) && $e->isa('Percona::WebAPI::Exception::Request')) { + die $e; + } + else { + die "Unknown error: $e"; + } + } + + return; +} + +sub _set { + my ($self, %args) = @_; + my @required_args = qw(method resources link); + my ($method, $res, $link) = @args{@required_args}; + + my $content; + if ( ref($res) eq 'ARRAY' ) { + $content = '[' . join(",\n", map { $_->as_json } @$res) . ']'; + } + elsif ( -f $res ) { + PTDEBUG && _d('Reading content from file', $res); + $content = '['; + my $data = do { + local $INPUT_RECORD_SEPARATOR = undef; + open my $fh, '<', $res + or die "Error opening $res: $OS_ERROR"; + <$fh>; + }; + $data =~ s/,?\s*$/]/; + $content .= $data; + } + else { + $content = $res->as_json; + } + + eval { + $self->_request( + method => $method, + url => $link, + content => $content, + ); + }; + if ( my $e = $EVAL_ERROR ) { + if (blessed($e) && $e->isa('Percona::WebAPI::Exception::Request')) { + die $e; + } + else { + die "Unknown error: $e"; + } + } + + my $links; + eval { + $links = decode_json($self->response->content); + }; + if ( $EVAL_ERROR ) { + warn sprintf "Error decoding resource: %s: %s", + $self->response->content, + $EVAL_ERROR; + return; + } + + $self->update_links($links); + + return; +} + +sub _request { + my ($self, %args) = @_; + + my @required_args = ( + 'method', + 'url', + ); + my ($method, $url) = @args{@required_args}; + + my @optional_args = ( + 'content', + 'headers', + ); + my ($content, $headers) = @args{@optional_args}; + + my $req = HTTP::Request->new($method => $url); + $req->content($content) if $content; + if ( uc($method) eq 'DELETE' ) { + $self->ua->default_header('Content-Length' => 0); + } + PTDEBUG && _d('Request', $method, $url, $req); + + my $res = $self->ua->request($req); + PTDEBUG && _d('Response', $res); + + if ( uc($method) eq 'DELETE' ) { + $self->ua->default_header('Content-Length' => undef); + } + + if ( !($res->code >= 200 && $res->code < 400) ) { + die Percona::WebAPI::Exception::Request->new( + method => $method, + url => $url, + content => $content, + status => $res->code, + error => $res->content, + ); + } + + $self->response($res); + + return; +} + +sub update_links { + my ($self, $new_links) = @_; + while (my ($svc, $links) = each %$new_links) { + while (my ($rel, $link) = each %$links) { + $self->links->{$svc}->{$rel} = $link; + } + } + PTDEBUG && _d('Updated links', $self->links); + return; +} + +1; diff --git a/lib/Percona/WebAPI/Exception/Request.pm b/lib/Percona/WebAPI/Exception/Request.pm new file mode 100644 index 00000000..dc80103e --- /dev/null +++ b/lib/Percona/WebAPI/Exception/Request.pm @@ -0,0 +1,44 @@ +package Percona::WebAPI::Exception::Request; + +use Mo; +use overload '""' => \&as_string; + +has 'method' => ( + is => 'ro', + isa => 'Str', + required => 1, +); + +has 'url' => ( + is => 'ro', + isa => 'Str', + required => 1, +); + +has 'content' => ( + is => 'ro', + isa => 'Maybe[Str]', + required => 0, +); + +has 'status' => ( + is => 'ro', + isa => 'Int', + required => 1, +); + +has 'error' => ( + is => 'ro', + isa => 'Str', + required => 1, +); + +sub as_string { + my $self = shift; + chomp(my $error = $self->error); + $error =~ s/\n/ /g; + return sprintf "Error: %s\nStatus: %d\nRequest: %s %s %s\n", + $error, $self->status, $self->method, $self->url, $self->content || ''; +} + +1; diff --git a/lib/Percona/WebAPI/Representation/HashRef.pm b/lib/Percona/WebAPI/Representation/HashRef.pm new file mode 100644 index 00000000..73598591 --- /dev/null +++ b/lib/Percona/WebAPI/Representation/HashRef.pm @@ -0,0 +1,18 @@ +package Percona::WebAPI::Representation::HashRef; + +use Moose::Role; + +sub as_hashref { + my ($self) = @_; + + # Copy the object into a new hashref. + my $as_hashref = { %$self }; + + # Delete the links because they're just for client-side use + # and the caller should be sending this object, not getting it. + delete $as_hashref->{links}; + + return $as_hashref; +} + +1; diff --git a/lib/Percona/WebAPI/Representation/JSON.pm b/lib/Percona/WebAPI/Representation/JSON.pm new file mode 100644 index 00000000..64222bdd --- /dev/null +++ b/lib/Percona/WebAPI/Representation/JSON.pm @@ -0,0 +1,19 @@ +package Percona::WebAPI::Representation::JSON; + +use Moose::Role; +use JSON; + +sub as_json { + my ($self) = @_; + + # Copy the object into a new hashref. + my $as_hashref = { %$self }; + + # Delete the links because they're just for client-side use + # and the caller should be sending this object, not getting it. + delete $as_hashref->{links}; + + return encode_json($as_hashref); +} + +1; diff --git a/lib/Percona/WebAPI/Resource/Agent.pm b/lib/Percona/WebAPI/Resource/Agent.pm new file mode 100644 index 00000000..29859d9d --- /dev/null +++ b/lib/Percona/WebAPI/Resource/Agent.pm @@ -0,0 +1,28 @@ +package Percona::WebAPI::Resource::Agent; + +use Mo; + +with 'Percona::WebAPI::Representation::JSON'; +with 'Percona::WebAPI::Representation::HashRef'; + +has 'id' => ( + is => 'ro', + isa => 'Str', + required => 1, +); + +has 'hostname' => ( + is => 'ro', + isa => 'Str', + required => 1, + default => sub { return `hostname 2>/dev/null` }, +); + +has 'versions' => ( + is => 'ro', + isa => 'Maybe[Percona::WebAPI::Resource::Versions]', + required => 0, + default => undef, +); + +1; diff --git a/lib/Percona/WebAPI/Resource/Config.pm b/lib/Percona/WebAPI/Resource/Config.pm new file mode 100644 index 00000000..48ee415c --- /dev/null +++ b/lib/Percona/WebAPI/Resource/Config.pm @@ -0,0 +1,14 @@ +package Percona::WebAPI::Resource::Config; + +use Mo; + +with 'Percona::WebAPI::Representation::JSON'; +with 'Percona::WebAPI::Representation::HashRef'; + +has 'options' => ( + is => 'ro', + isa => 'HashRef', + required => 1, +); + +1; diff --git a/lib/Percona/WebAPI/Resource/Run.pm b/lib/Percona/WebAPI/Resource/Run.pm new file mode 100644 index 00000000..7c12bbaa --- /dev/null +++ b/lib/Percona/WebAPI/Resource/Run.pm @@ -0,0 +1,32 @@ +package Percona::WebAPI::Resource::Run; + +use Mo; + +with 'Percona::WebAPI::Representation::JSON'; +with 'Percona::WebAPI::Representation::HashRef'; + +has 'number' => ( + is => 'ro', + isa => 'Int', + required => 1, +); + +has 'program' => ( + is => 'ro', + isa => 'Str', + required => 1, +); + +has 'options' => ( + is => 'ro', + isa => 'Maybe[Str]', + required => 0, +); + +has 'output' => ( + is => 'ro', + isa => 'Str', + required => 1, +); + +1; diff --git a/lib/Percona/WebAPI/Resource/Service.pm b/lib/Percona/WebAPI/Resource/Service.pm new file mode 100644 index 00000000..de456ffb --- /dev/null +++ b/lib/Percona/WebAPI/Resource/Service.pm @@ -0,0 +1,26 @@ +package Percona::WebAPI::Resource::Service; + +use Mo; + +with 'Percona::WebAPI::Representation::JSON'; +with 'Percona::WebAPI::Representation::HashRef'; + +has 'name' => ( + is => 'ro', + isa => 'Str', + required => 1, +); + +has 'schedule' => ( + is => 'ro', + isa => 'Str', + required => 1, +); + +has 'run' => ( + is => 'ro', + isa => 'ArrayRef[Percona::WebAPI::Resource::Run]', + required => 1, +); + +1; diff --git a/lib/Percona/WebAPI/Resource/Versions.pm b/lib/Percona/WebAPI/Resource/Versions.pm new file mode 100644 index 00000000..c2e96b32 --- /dev/null +++ b/lib/Percona/WebAPI/Resource/Versions.pm @@ -0,0 +1,14 @@ +package Percona::WebAPI::Resource::Versions; + +use Mo; + +with 'Percona::WebAPI::Representation::JSON'; +with 'Percona::WebAPI::Representation::HashRef'; + +has 'versions' => ( + is => 'ro', + isa => 'HashRef', + required => 1, +); + +1;