#!/usr/bin/perl # This program tests a MySQL server against the MySQL option and variable # reference. # # This program is copyright (c) 2007 Baron Schwartz. # 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. use strict; use warnings FATAL => 'all'; use DBI; use English qw(-no_match_vars); use Getopt::Long; use List::Util qw(max); use Term::ReadKey; use HTML::TableExtract; use WWW::Mechanize; our $VERSION = '@VERSION@'; our $DISTRIB = '@DISTRIB@'; our $SVN_REV = sprintf("%d", q$Revision: 859 $ =~ m/(\d+)/g); # ############################################################################ # Get configuration information. # ############################################################################ # Define cmdline args; each is GetOpt::Long spec, whether required, # human-readable description. Add more hash entries as needed. my @opt_spec = ( { s => 'askpass', d => 'Prompt for password for connections' }, { s => 'database|D=s', d => 'Database to use' }, { s => 'defaults-file|F=s', d => 'Only read default options from the given file' }, { s => 'host|h=s', d => 'Connect to host' }, { s => 'help', d => 'Show this help message' }, { s => 'password|p=s', d => 'Password to use when connecting' }, { s => 'port|P=i', d => 'Port number to use for connection' }, { s => 'socket|S=s', d => 'Socket file to use for connection' }, { s => 'user|u=s', d => 'User for login if not current user' }, { s => 'version', d => 'Output version information and exit' }, ); # This is the container for the command-line options' values to be stored in # after processing. Initial values are defaults. my %opts; # Post-process... my %opt_seen; foreach my $spec ( @opt_spec ) { my ( $long, $short ) = $spec->{s} =~ m/^([\w-]+)(?:\|([^!+=]*))?/; $spec->{k} = $short || $long; $spec->{l} = $long; $spec->{t} = $short; $spec->{n} = $spec->{s} =~ m/!/; $opts{$spec->{k}} = undef unless defined $opts{$spec->{k}}; die "Duplicate option $spec->{k}" if $opt_seen{$spec->{k}}++; } Getopt::Long::Configure('no_ignore_case', 'bundling'); GetOptions( map { $_->{s} => \$opts{$_->{k}} } @opt_spec) or $opts{help} = 1; if ( $opts{version} ) { print "$PROGRAM_NAME Ver $VERSION Distrib $DISTRIB Changeset $SVN_REV\n"; exit(0); } # If a filename or other argument(s) is required after the other arguments, # add "|| !@ARGV" inside the parens on the next line. if ( $opts{help} ) { print "Usage: $PROGRAM_NAME batch-file\n\n"; my $maxw = max(map { length($_->{l}) + ($_->{n} ? 4 : 0)} @opt_spec); foreach my $spec ( sort { $a->{l} cmp $b->{l} } @opt_spec ) { my $long = $spec->{n} ? "[no]$spec->{l}" : $spec->{l}; my $short = $spec->{t} ? "-$spec->{t}" : ''; printf(" --%-${maxw}s %-4s %s\n", $long, $short, $spec->{d}); } (my $usage = <<" USAGE") =~ s/^ //gm; $PROGRAM_NAME tests MySQL's option table reference (http://dev.mysql.com/doc/en/mysqld-option-tables.html). If possible, database options are read from your .my.cnf file. For more details, please read the documentation: perldoc $PROGRAM_NAME USAGE print $usage; exit(0); } # ############################################################################ # Get ready to do the main work. # ############################################################################ my %conn = ( F => 'mysql_read_default_file', h => 'host', P => 'port', S => 'mysql_socket' ); # Connect to the database if ( !$opts{p} && $opts{askpass} ) { print "Enter password: "; ReadMode('noecho'); chomp($opts{p} = ); ReadMode('normal'); print "\n"; } my $dsn = 'DBI:mysql:' . ( $opts{D} || '' ) . ';' . join(';', map { "$conn{$_}=$opts{$_}" } grep { defined $opts{$_} } qw(F h P S)) . ';mysql_read_default_group=mysql'; my $dbh = DBI->connect($dsn, @opts{qw(u p)}, { AutoCommit => 1, RaiseError => 1, PrintError => 0 } ); # Get the version and decide which URL to fetch my $version = sprintf('%d.%d', $dbh->{mysql_serverinfo} =~ m/(\d+)/g); my $fullver = sprintf('%d.%d.%d', $dbh->{mysql_serverinfo} =~ m/(\d+)/g); my $url_ver = $version lt '5.0' ? '4.1' : $version eq '5.0' ? '5.0' : $version eq '5.1' ? '5.1' : undef; die "Ack, I'm not able to deal with version $version" unless $url_ver; print "Testing table for $url_ver against MySQL $fullver\n"; my $url = "http://dev.mysql.com/doc/refman/$url_ver/en/mysqld-option-tables.html"; my $m = WWW::Mechanize->new(); $m->get($url); my $te = HTML::TableExtract->new(); $te->parse($m->content); my ($table) = $te->tables(); # Praise God, they don't use tables for layout. my @rows = $te->rows; shift @rows; # headers my %options; foreach my $row (@rows) { my ($varname) = $row->[0] =~ m/([\w-]+$)/; my %hash; @hash{qw(name cmd_line option_file system_var status_var var_scope dynamic)} = @$row; $options{$varname} = \%hash; } my %vars = %{ $dbh->selectall_hashref('show variables', 'Variable_name') }; my %stat = %{ $dbh->selectall_hashref('show status', 'Variable_name') }; # Look for ones not covered foreach my $key ( sort keys %vars ) { print "Undocumented system variable $key\n" unless exists $options{$key}; } foreach my $key ( sort keys %stat ) { print "Undocumented status variable $key\n" unless exists $options{$key}; } # Verify the scope of system variables (it's pretty hard to verify the scope of # status variables). foreach my $option ( sort keys %options ) { my $def = $options{$option}; (my $var = $option) =~ tr/-/_/; # See if it's a server variable. my ($is_global_var, $is_session_var, $can_global_set, $can_session_set); if ( $var eq $option || !exists $options{$var} ) { eval { $dbh->do('SELECT @@global.' . $var); $is_global_var = 1; $dbh->do('SET @@global.' . $var . ' := @@global.' . $var); $can_global_set = 1; }; eval { $dbh->do('SELECT @@session.' . $var); $is_session_var = 1; $dbh->do('SET @@session.' . $var . ' := @@session.' . $var); $can_session_set = 1; }; } # Check the scope and dynamism of the system variable if ( $def->{system_var} eq 'Y' ) { # Global if ( $def->{var_scope} =~ m/global|both/ && !$is_global_var ) { print "System variable $option is not a global variable\n"; } if ( $def->{var_scope} !~ m/global|both/ && $is_global_var ) { print "System variable $option is a global variable\n"; } if ( $def->{dynamic} =~ m/yes/ && !$can_global_set ) { print "System variable $option is not global dynamic\n"; } if ( $def->{dynamic} =~ m/no/ && $can_global_set ) { print "System variable $option is global dynamic\n"; } # Session if ( $def->{var_scope} =~ m/session|both/ && !$is_session_var ) { print "System variable $option is not a session variable\n"; } if ( $def->{var_scope} !~ m/session|both/ && $is_session_var ) { print "System variable $option is a session variable\n"; } if ( $def->{dynamic} =~ m/yes/ && !$can_session_set ) { print "System variable $option is not session dynamic\n"; } if ( $def->{dynamic} =~ m/no/ && $can_session_set ) { print "System variable $option is session dynamic\n"; } } if ( $def->{system_var} eq 'Y' && !defined $vars{$option} && !defined $vars{$var} ) { print "No such system variable $option\n"; } if ( $def->{status_var} eq 'Y' && !defined $stat{$option} ) { print "No such status variable $option\n"; } }