diff --git a/lib/bash/alt_cmds.sh b/lib/bash/alt_cmds.sh new file mode 100644 index 00000000..55a41e3a --- /dev/null +++ b/lib/bash/alt_cmds.sh @@ -0,0 +1,34 @@ +# This program is copyright 2011 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. +# ########################################################################### +# alt_cmds package +# ########################################################################### + +# Package: alt_cmds +# alt_cmds provides alternatives to commands that aren't on all systems. + +set -u + +# seq N, return 1, ..., 5 +_seq() { + local i=$1 + awk "BEGIN { for(i=1; i<=$i; i++) print i; }" +} + +# ########################################################################### +# End alt_cmds package +# ########################################################################### diff --git a/lib/bash/collect.sh b/lib/bash/collect.sh new file mode 100644 index 00000000..9acf2a0c --- /dev/null +++ b/lib/bash/collect.sh @@ -0,0 +1,294 @@ +# This program is copyright 2011 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. +# ########################################################################### +# collect package +# ########################################################################### + +# Package: collect +# collect collects system information. + +set -u + +# Global variables. +CMD_GDB=${CMD_GDB:-"gdb"} +CMD_IOSTAT=${CMD_IOSTAT:-"iostat"} +CMD_MPSTAT=${CMD_MPSTAT:-"mpstat"} +CMD_MYSQL=${CMD_MSSQL:-"mysql"} +CMD_MYSQLADMIN=${CMD_MYSQL_ADMIN:-"mysqladmin"} +CMD_OPCONTROL=${CMD_OPCONTROL:-"opcontrol"} +CMD_OPREPORT=${CMD_OPREPORT:-"opreport"} +CMD_PMAP=${CMD_PMAP:-"pmap"} +CMD_STRACE=${CMD_STRACE:-"strace"} +CMD_TCPDUMP=${CMD_TCPDUMP:-"tcpdump"} +CMD_VMSTAT=${CMD_VMSTAT:-"vmstat"} + +collect() { + local d=$1 # directory to save results in + local p=$2 # prefix for each result file + + # Get pidof mysqld; pidof doesn't exist on some systems. We try our best... + local mysqld_pid=$(pidof -s mysqld); + if [ -z "$mysqld_pid" ]; then + mysqld_pid=$(pgrep -o -x mysqld); + fi + if [ -z "$mysqld_pid" ]; then + mysqld_pid=$(ps -eaf | grep 'mysql[d]' | grep -v mysqld_safe | awk '{print $2}' | head -n1); + fi + + # Get memory allocation info before anything else. + if [ -x "$CMD_PMAP" -a "$mysqld_pid" ]; then + if $CMD_PMAP --help 2>&1 | grep -- -x >/dev/null 2>&1 ; then + $CMD_PMAP -x $mysqld_pid > "$d/$p-pmap" + else + # Some pmap's apparently don't support -x (issue 116). + $CMD_PMAP $mysqld_pid > "$d/$p-pmap" + fi + fi + + # Getting a GDB stacktrace can be an intensive operation, + # so do this only if necessary. + if [ "$OPT_COLLECT_GDB" = "yes" -a "$mysqld_pid" ]; then + $CMD_GDB \ + -ex "set pagination 0" \ + -ex "thread apply all bt" \ + --batch -p $mysqld_pid \ + >> "$d/$p-stacktrace" + else + echo "GDB (--collect-gdb) was not enabled" >> "$d/$p-stacktrace" + fi + + # Get MySQL's variables if possible. Then sleep long enough that we probably + # complete SHOW VARIABLES if all's well. (We don't want to run mysql in the + # foreground, because it could hang.) + $CMD_MYSQL $EXT_ARGV -e 'SHOW GLOBAL VARIABLES' >> "$d/$p-variables" 2>&1 & + sleep .2 + + # Get the major.minor version number. Version 3.23 doesn't matter for our + # purposes, and other releases have x.x.x* version conventions so far. + local mysql_version="$(awk '/^version[^_]/{print substr($2,1,3)}' "$d/$p-variables")" + + # Is MySQL logging its errors to a file? If so, tail that file. + local mysql_error_log="$(awk '/log_error/{print $2}' "$d/$p-variables")" + if [ -z "$mysql_error_log" -a "$mysqld_pid" ]; then + # Try getting it from the open filehandle... + mysql_error_log="$(ls -l /proc/$mysqld_pid/fd | awk '/ 2 ->/{print $NF}')" + fi + + local tail_error_log_pid="" + if [ "$mysql_error_log" ]; then + echo "The MySQL error log seems to be ${mysql_error_log}" + tail -f "$mysql_error_log" >"$d/$p-log_error" 2>&1 & + tail_error_log_pid=$! + # Send a mysqladmin debug to the server so we can potentially learn about + # locking etc. + $CMD_MYSQLADMIN $EXT_ARGV debug + else + echo "Could not find the MySQL error log" + fi + + # Get a sample of these right away, so we can get these without interaction + # with the other commands we're about to run. + local innostat="SHOW /*!40100 ENGINE*/ INNODB STATUS\G" + if [ "${mysql_version}" '>' "5.1" ]; then + local mutex="SHOW ENGINE INNODB MUTEX" + else + local mutex="SHOW MUTEX STATUS" + fi + $CMD_MYSQL $EXT_ARGV -e "$innostat" >> "$d/$p-innodbstatus1" 2>&1 & + $CMD_MYSQL $EXT_ARGV -e "$mutex" >> "$d/$p-mutex-status1" 2>&1 & + open_tables >> "$d/$p-opentables1" 2>&1 & + + # If TCP dumping is specified, start that on the server's port. + local tcpdump_pid="" + if [ "$OPT_COLLECT_TCPDUMP" = "yes" ]; then + local port=$(awk '/^port/{print $2}' "$d/$p-variables") + if [ "$port" ]; then + $CMD_TCPDUMP -i any -s 4096 -w "$d/$p-tcpdump" port ${port} & + tcpdump_pid=$! + fi + fi + + # Next, start oprofile gathering data during the whole rest of this process. + # The --init should be a no-op if it has already been init-ed. + local have_oprofile="no" + if [ "$OPT_COLLECT_OPROFILE" = "yes" ]; then + if $CMD_OPCONTROL --init; then + $CMD_OPCONTROL --start --no-vmlinux + have_oprofile="yes" + fi + elif [ "$OPT_COLLECT_STRACE" = "yes" ]; then + # Don't run oprofile and strace at the same time. + $CMD_STRACE -T -s 0 -f -p $mysqld_pid > "${DEST}/$d-strace" 2>&1 & + local strace_pid=$! + fi + + # Grab a few general things first. Background all of these so we can start + # them all up as quickly as possible. + ps -eaf >> "$d/$p-ps" 2>&1 & + sysctl -a >> "$d/$p-sysctl" 2>&1 & + top -bn1 >> "$d/$p-top" 2>&1 & + $CMD_VMSTAT 1 $OPT_INTERVAL >> "$d/$p-vmstat" 2>&1 & + $CMD_VMSTAT $OPT_INTERVAL 2 >> "$d/$p-vmstat-overall" 2>&1 & + $CMD_IOSTAT -dx 1 $OPT_INTERVAL >> "$d/$p-iostat" 2>&1 & + $CMD_IOSTAT -dx $OPT_INTERVAL 2 >> "$d/$p-iostat-overall" 2>&1 & + $CMD_MPSTAT -P ALL 1 $OPT_INTERVAL >> "$d/$p-mpstat" 2>&1 & + $CMD_MPSTAT -P ALL $OPT_INTERVAL 1 >> "$d/$p-mpstat-overall" 2>&1 & + lsof -nP -p $mysqld_pid -bw >> "$d/$p-lsof" 2>&1 & + + # Collect multiple snapshots of the status variables. We use + # mysqladmin -c even though it is buggy and won't stop on its + # own in 5.1 and newer, because there is a chance that we will + # get and keep a connection to the database; in troubled times + # the database tends to exceed max_connections, so reconnecting + # in the loop tends not to work very well. + $CMD_MYSQLADMIN $EXT_ARGV ext -i1 -c$OPT_RUN_TIME >>"$d/$p-mysqladmin" 2>&1 & + local mysqladmin_pid=$! + + local have_lock_waits_table=0 + $CMD_MYSQL $EXT_ARGV -e "SHOW TABLES FROM INFORMATION_SCHEMA" \ + | grep -qi "INNODB_LOCK_WAITS" + if [ $? -eq 0 ]; then + have_lock_waits_table=1 + fi + + # This loop gathers data for the rest of the duration, and defines the time + # of the whole job. + echo "Loop start: $(date +'TS %s.%N %F %T')" + for loopno in $(_seq $OPT_RUN_TIME); do + # We check the disk, but don't exit, because we need to stop jobs if we + # need to exit. + disk_space $d > $d/$p-disk-space + check_disk_space \ + $d/$p-disk-space \ + "$OPT_DISK_BYTE_LIMIT" \ + "$OPT_DISK_PCT_LIMIT" \ + || break + + # Synchronize ourselves onto the clock tick, so the sleeps are 1-second + sleep $(date +%s.%N | awk '{print 1 - ($1 % 1)}') + local ts="$(date +"TS %s.%N %F %T")" + + # Collect the stuff for this cycle + (cat /proc/diskstats 2>&1; echo $ts) >> "$d/$p-diskstats" & + (cat /proc/stat 2>&1; echo $ts) >> "$d/$p-procstat" & + (cat /proc/vmstat 2>&1; echo $ts) >> "$d/$p-procvmstat" & + (cat /proc/meminfo 2>&1; echo $ts) >> "$d/$p-meminfo" & + (cat /proc/slabinfo 2>&1; echo $ts) >> "$d/$p-slabinfo" & + (cat /proc/interrupts 2>&1; echo $ts) >> "$d/$p-interrupts" & + (df -h 2>&1; echo $ts) >> "$d/$p-df" & + (netstat -antp 2>&1; echo $ts) >> "$d/$p-netstat" & + (netstat -s 2>&1; echo $ts) >> "$d/$p-netstat_s" & + + ($CMD_MYSQL $EXT_ARGV -e "SHOW FULL PROCESSLIST\G" 2>&1; echo $ts) \ + >> "$d/$p-processlist" + + if [ $have_lock_waits_table -eq 1 ]; then + (lock_waits 2>&1; echo $ts) >>"$d/$p-lock-waits" + fi + done + echo "Loop end: $(date +'TS %s.%N %F %T')" + + if [ "$have_oprofile" = "yes" ]; then + $CMD_OPCONTROL --stop + $CMD_OPCONTROL --dump + kill $(pidof oprofiled); # TODO: what if system doesn't have pidof? + $CMD_OPCONTROL --save=pt_collect_$p + + # Attempt to generate a report; if this fails, then just tell the user + # how to generate the report. + local mysqld_path=$(which mysqld); + if [ "$mysqld_path" -a -f "$mysqld_path" ]; then + $CMD_OPREPORT \ + --demangle=smart \ + --symbols \ + --merge tgid \ + session:pt_collect_$p \ + "$mysqld_path" \ + > "$d/$p-opreport" + else + echo "oprofile data saved to pt_collect_$p; you should be able" \ + "to get a report by running something like 'opreport" \ + "--demangle=smart --symbols --merge tgid session:pt_collect_$p" \ + "/path/to/mysqld'" \ + > "$d/$p-opreport" + fi + elif [ "$OPT_COLLECT_STRACE" = "yes" ]; then + kill -s 2 $strace_pid + sleep 1 + kill -s 15 $strace_pid + # Sometimes strace leaves threads/processes in T status. + kill -s 18 $mysqld_pid + fi + + $CMD_MYSQL $EXT_ARGV -e "$innostat" >> "$d/$p-innodbstatus2" 2>&1 & + $CMD_MYSQL $EXT_ARGV -e "$mutex" >> "$d/$p-mutex-status2" 2>&1 & + open_tables >> "$d/$p-opentables2" 2>&1 & + + # Kill backgrounded tasks. + kill $mysqladmin_pid + [ "$tail_error_log_pid" ] && kill $tail_error_log_pid + [ "$tcpdump_pid" ] && kill $tcpdump_pid + + # Finally, record what system we collected this data from. + hostname > "$d/$p-hostname" +} + +open_tables() { + local open_tables=$($CMD_MYSQLADMIN $EXT_ARGV ext | grep "Open_tables" | awk '{print $4}') + if [ -n "$open_tables" -a $open_tables -le 1000 ]; then + $CMD_MYSQL $EXT_ARGV -e 'SHOW OPEN TABLES' 2>&1 & + else + echo "Too many open tables: $open_tables" + fi +} + +lock_waits() { + local sql1="SELECT + CONCAT('thread ', b.trx_mysql_thread_id, ' from ', p.host) AS who_blocks, + IF(p.command = \"Sleep\", p.time, 0) AS idle_in_trx, + MAX(TIMESTAMPDIFF(SECOND, r.trx_wait_started, CURRENT_TIMESTAMP)) AS max_wait_time, + COUNT(*) AS num_waiters + FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS AS w + INNER JOIN INFORMATION_SCHEMA.INNODB_TRX AS b ON b.trx_id = w.blocking_trx_id + INNER JOIN INFORMATION_SCHEMA.INNODB_TRX AS r ON r.trx_id = w.requesting_trx_id + LEFT JOIN INFORMATION_SCHEMA.PROCESSLIST AS p ON p.id = b.trx_mysql_thread_id + GROUP BY who_blocks ORDER BY num_waiters DESC\G" + $CMD_MYSQL $EXT_ARGV -e "$sql1" + + local sql2="SELECT + r.trx_id AS waiting_trx_id, + r.trx_mysql_thread_id AS waiting_thread, + TIMESTAMPDIFF(SECOND, r.trx_wait_started, CURRENT_TIMESTAMP) AS wait_time, + r.trx_query AS waiting_query, + l.lock_table AS waiting_table_lock, + b.trx_id AS blocking_trx_id, b.trx_mysql_thread_id AS blocking_thread, + SUBSTRING(p.host, 1, INSTR(p.host, ':') - 1) AS blocking_host, + SUBSTRING(p.host, INSTR(p.host, ':') +1) AS blocking_port, + IF(p.command = \"Sleep\", p.time, 0) AS idle_in_trx, + b.trx_query AS blocking_query + FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS AS w + INNER JOIN INFORMATION_SCHEMA.INNODB_TRX AS b ON b.trx_id = w.blocking_trx_id + INNER JOIN INFORMATION_SCHEMA.INNODB_TRX AS r ON r.trx_id = w.requesting_trx_id + INNER JOIN INFORMATION_SCHEMA.INNODB_LOCKS AS l ON w.requested_lock_id = l.lock_id + LEFT JOIN INFORMATION_SCHEMA.PROCESSLIST AS p ON p.id = b.trx_mysql_thread_id + ORDER BY wait_time DESC\G" + $CMD_MYSQL $EXT_ARGV -e "$sql2" +} + +# ########################################################################### +# End collect package +# ########################################################################### diff --git a/lib/bash/daemon.sh b/lib/bash/daemon.sh new file mode 100644 index 00000000..1fe823b8 --- /dev/null +++ b/lib/bash/daemon.sh @@ -0,0 +1,71 @@ +# This program is copyright 2011 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. +# ########################################################################### +# daemon package +# ########################################################################### + +# Package: daemon +# daemon handles daemon related tasks like checking a PID file. + +set -u + +# Sub: make_pid_file +# Check and make a PID file. +# +# Arguments: +# file - File to write PID to. +# pid - PID to write into file. +make_pid_file() { + local file=$1 + local pid=$2 + + # Yes there's a race condition here, between checking if the file exists + # and creating it, but it's not important enough to handle. + + if [ -f "$file" ]; then + # PID file already exists. See if the pid it contains is still running. + # If yes, then die. Else, the pid file is stale and we can reclaim it. + local old_pid=$(cat $file) + if [ -z "$old_pid" ]; then + # PID file is empty, so be safe and die since we can't check a + # non-existent pid. + die "PID file $file already exists but it is empty" + else + kill -0 $old_pid 2>/dev/null + if [ $? -eq 0 ]; then + die "PID file $file already exists and its PID ($old_pid) is running" + else + echo "Overwriting PID file $file because its PID ($old_pid)" \ + "is not running" + fi + fi + fi + + # PID file doesn't exist, or it does but its pid is stale. + echo "$pid" > $file +} + +remove_pid_file() { + local file=$1 + if [ -f "$file" ]; then + rm $file + fi +} + +# ########################################################################### +# End daemon package +# ########################################################################### diff --git a/lib/bash/log_warn_die.sh b/lib/bash/log_warn_die.sh new file mode 100644 index 00000000..6d5ca2e4 --- /dev/null +++ b/lib/bash/log_warn_die.sh @@ -0,0 +1,46 @@ +# This program is copyright 2011 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. +# ########################################################################### +# log_warn_die package +# ########################################################################### + +# Package: log_warn_die +# log_warn_die provides standard log(), warn(), and die() subs. + +set -u + +# Global variables. +EXIT_STATUS=0 + +log() { + TS=$(date +%F-%T | tr :- _); + echo "$TS $1" +} + +warn() { + log "$1" >&2 + EXIT_STATUS=$((EXIT_STATUS | 1)) +} + +die() { + warn "$1" + exit 1 +} + +# ########################################################################### +# End log_warn_die package +# ########################################################################### diff --git a/lib/bash/parse_options.sh b/lib/bash/parse_options.sh new file mode 100644 index 00000000..e4915247 --- /dev/null +++ b/lib/bash/parse_options.sh @@ -0,0 +1,286 @@ +# This program is copyright 2011 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. +# ########################################################################### +# parse_options package +# ########################################################################### + +# Package: parse_options +# parse_options parses Perl POD options from Bash tools and creates +# global variables for each option. + +set -u + +# Global variables. These must be global because declare inside a +# sub will be scoped locally. +ARGV="" # Non-option args (probably input files) +EXT_ARGV="" # Everything after -- (args for an external command) +OPT_ERRS=0 # How many command line option errors +OPT_VERSION="no" # If --version was specified +OPT_HELP="no" # If --help was specified + +# Sub: usage +# Print usage (--help) and list the program's options. +# +# Arguments: +# file - Program file with Perl POD which has usage and options. +# +# Required Global Variables: +# TIMDIR - Temp directory set by . +# TOOL - Tool's name. +# +# Optional Global Variables: +# OPT_ERR - Command line option error message. +usage() { + local file=$1 + + local usage=$(grep '^Usage: ' $file) + echo $usage >&2 + echo >&2 + echo "For more information, 'man $TOOL' or 'perldoc $file'." >&2 +} + +usage_or_errors() { + local file=$1 + + if [ "$OPT_VERSION" = "yes" ]; then + local version=$(grep '^pt-[^ ]\+ [0-9]' $file) + echo "$version" + return 1 + fi + + if [ "$OPT_HELP" = "yes" ]; then + usage "$file" + echo >&2 + echo "Command line options:" >&2 + echo >&2 + for opt in $(ls $TMPDIR/po/); do + local desc=$(cat $TMPDIR/po/$opt | grep '^desc:' | sed -e 's/^desc://') + echo "--$opt" >&2 + echo " $desc" >&2 + echo >&2 + done + return 1 + fi + + if [ $OPT_ERRS -gt 0 ]; then + echo >&2 + usage $file + return 1 + fi + + # No --help, --version, or errors. + return 0 +} + +# Sub: parse_options +# Parse Perl POD options from a program file. +# +# Arguments: +# file - Program file with Perl POD options. +# +# Required Global Variables: +# TIMDIR - Temp directory set by . +# +# Set Global Variables: +# This sub decalres a global var for each option by uppercasing the +# option, removing the option's leading --, changing all - to _, and +# prefixing with "OPT_". E.g. --foo-bar becomes OPT_FOO_BAR. +parse_options() { + local file=$1 + shift + + # Parse the program options (po) from the POD. Each option has + # a spec file like: + # $ cat po/string-opt2 + # long=string-opt2 + # type=string + # default=foo + # That's the spec for --string-opt2. Each line is a key:value pair + # from the option's POD line like "type: string; default: foo". + mkdir $TMPDIR/po/ 2>/dev/null + rm -rf $TMPDIR/po/* + ( + export PO_DIR="$TMPDIR/po" + cat $file | perl -ne ' + BEGIN { $/ = ""; } + next unless $_ =~ m/^=head1 OPTIONS/; + while ( defined(my $para = <>) ) { + last if $para =~ m/^=head1/; + chomp; + if ( $para =~ m/^=item --(\S+)/ ) { + my $opt = $1; + my $file = "$ENV{PO_DIR}/$opt"; + open my $opt_fh, ">", $file or die "Cannot open $file: $!"; + printf $opt_fh "long:$opt\n"; + $para = <>; + chomp; + if ( $para =~ m/^[a-z ]+:/ ) { + map { + chomp; + my ($attrib, $val) = split(/: /, $_); + printf $opt_fh "$attrib:$val\n"; + } split(/; /, $para); + $para = <>; + chomp; + } + my ($desc) = $para =~ m/^([^?.]+)/; + printf $opt_fh "desc:$desc.\n"; + close $opt_fh; + } + } + last; + ' + ) + + # Evaluate the program options into existence as global variables + # transformed like --my-op == $OPT_MY_OP. If an option has a default + # value, it's assigned that value. Else, it's value is an empty string. + for opt_spec in $(ls $TMPDIR/po/); do + local opt="" + local default_val="" + local neg=0 + while read line; do + local key=`echo $line | cut -d ':' -f 1` + local val=`echo $line | cut -d ':' -f 2` + case "$key" in + long) + opt=$(echo $val | sed 's/-/_/g' | tr [:lower:] [:upper:]) + ;; + default) + default_val="$val" + ;; + "short form") + ;; + type) + ;; + desc) + ;; + negatable) + if [ "$val" = "yes" ]; then + neg=1 + fi + ;; + *) + echo "Invalid attribute in $TMPDIR/po/$opt_spec: $line" >&2 + exit 1 + esac + done < $TMPDIR/po/$opt_spec + + if [ -z "$opt" ]; then + echo "No long attribute in option spec $TMPDIR/po/$opt_spec" >&2 + exit 1 + fi + + if [ $neg -eq 1 ]; then + if [ -z "$default_val" ] || [ "$default_val" != "yes" ]; then + echo "Option $opt_spec is negatable but not default: yes" >&2 + exit 1 + fi + fi + + eval "OPT_${opt}"="$default_val" + done + + # Parse the command line options. Anything after -- is put into + # EXT_ARGV. Options must begin with one or two hyphens (--help or -h), + # else the item is put into ARGV (it's probably a filename, directory, + # etc.) The program option specs parsed above are used to valid the + # command line options. All options have already been eval'd into + # existence, but we re-eval opts specified on the command line to update + # the corresponding global variable's value. For example, if --foo has + # a default value 100, then $OPT_FOO=100 already, but if --foo=500 is + # specified on the command line, then we re-eval $OPT_FOO=500 to update + # $OPT_FOO. + for opt; do + if [ $# -eq 0 ]; then + break # no more opts + fi + opt=$1 + if [ "$opt" = "--" ]; then + shift + EXT_ARGV="$@" + break + fi + shift + if [ $(expr "$opt" : "-") -eq 0 ]; then + # Option does not begin with a hyphen (-), so treat it as + # a filename, directory, etc. + if [ -z "$ARGV" ]; then + ARGV="$opt" + else + ARGV="$ARGV $opt" + fi + continue + fi + + # Save real opt from cmd line for error messages. + local real_opt="$opt" + + # Strip leading -- or --no- from option. + if $(echo $opt | grep -q '^--no-'); then + neg=1 + opt=$(echo $opt | sed 's/^--no-//') + else + neg=0 + opt=$(echo $opt | sed 's/^-*//') + fi + + # Find the option's spec file. + if [ -f "$TMPDIR/po/$opt" ]; then + spec="$TMPDIR/po/$opt" + else + spec=$(grep "^short form:-$opt\$" $TMPDIR/po/* | cut -d ':' -f 1) + if [ -z "$spec" ]; then + OPT_ERRS=$(($OPT_ERRS + 1)) + echo "Unknown option: $real_opt" >&2 + continue + fi + fi + + # Get the value specified for the option, if any. If the opt's spec + # says it has a type, then it requires a value and that value should + # be the next item ($1). Else, typeless options (like --version) are + # either "yes" if specified, else "no" if negatable and --no-opt. + required_arg=$(cat $spec | grep '^type:' | cut -d':' -f2) + if [ -n "$required_arg" ]; then + if [ $# -eq 0 ]; then + OPT_ERRS=$(($OPT_ERRS + 1)) + echo "$real_opt requires a $required_arg argument" >&2 + continue + else + val="$1" + shift + fi + else + if [ $neg -eq 0 ]; then + val="yes" + else + val="no" + fi + fi + + # Get and transform the opt's long form. E.g.: -q == --quiet == QUIET. + opt=$(cat $spec | grep '^long:' | cut -d':' -f2 | sed 's/-/_/g' | tr [:lower:] [:upper:]) + + # Re-eval the option to update its global variable value. + eval "OPT_$opt"="$val" + done +} + +# ########################################################################### +# End parse_options package +# ########################################################################### diff --git a/lib/bash/safeguards.sh b/lib/bash/safeguards.sh new file mode 100644 index 00000000..1e6df342 --- /dev/null +++ b/lib/bash/safeguards.sh @@ -0,0 +1,80 @@ +# This program is copyright 2011 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. +# ########################################################################### +# safeguards package +# ########################################################################### + +# Package: safeguards +# safeguards is a collection of function to help avoid blowing things up. + +set -u + +disk_space() { + local filesystem=${1:-"$PWD"} + # Filesystem 1024-blocks Used Available Capacity Mounted on + # /dev/disk0s2 118153176 94409664 23487512 81% / + df -P -k $filesystem +} + +# Sub: check_disk_space +# Check if there is or will be enough disk space. Input is a file +# with output from , i.e. `df -P -k`. The df output +# must use 1k blocks, but the mb arg from the user is in MB. +# +# Arguments: +# file - File with output from . +# mb - Minimum MB free. +# pc - Minimum percent free. +# mb_margin - Add this many MB to the real MB used. +# +# Returns: +# 0 if there is/will be enough disk space, else 1. +check_disk_space() { + local file=$1 + local mb=${2:-"0"} + local pc=${3:-"0"} + local mb_margin=${4:-"0"} + + # Convert MB to KB because the df output should be in 1k blocks. + local kb=$(($mb * 1024)) + local kb_margin=$(($mb_margin * 1024)) + + local kb_used=$(cat $file | awk '/^\//{print $3}'); + local kb_free=$(cat $file | awk '/^\//{print $4}'); + local pc_used=$(cat $file | awk '/^\//{print $5}' | sed -e 's/%//g'); + + if [ "$kb_margin" -gt "0" ]; then + local kb_total=$(($kb_used + $kb_free)) + + kb_used=$(($kb_used + $kb_margin)) + kb_free=$(($kb_free - $kb_margin)) + pc_used=$(awk "BEGIN { printf(\"%d\", $kb_used/$kb_total * 100) }") + fi + + local pc_free=$((100 - $pc_used)) + + if [ "$kb_free" -le "$kb" -o "$pc_free" -le "$pc" ]; then + warn "Not enough free disk space: ${pc_free}% free, ${kb_free} KB free; wanted more than ${pc}% free or ${kb} KB free" + return 1 + fi + + return 0 +} + +# ########################################################################### +# End safeguards package +# ########################################################################### diff --git a/lib/bash/tmpdir.sh b/lib/bash/tmpdir.sh new file mode 100644 index 00000000..d1b9e1b7 --- /dev/null +++ b/lib/bash/tmpdir.sh @@ -0,0 +1,70 @@ +# This program is copyright 2011 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. +# ########################################################################### +# tmpdir package +# ########################################################################### + +# Package: tmpdir +# tmpdir make a secure temporary directory using mktemp. + +set -u + +# Global variables. +TMPDIR="" + +# Sub: mk_tmpdir +# Create a secure tmpdir and set TMPDIR. +# +# Optional Arguments: +# dir - User-specified tmpdir (default none). +# +# Set Global Variables: +# TMPDIR - Absolute path of secure temp directory. +mk_tmpdir() { + local dir=${1:-""} + + if [ -n "$dir" ]; then + if [ ! -d "$dir" ]; then + mkdir $dir || die "Cannot make tmpdir $dir" + fi + TMPDIR="$dir" + else + local tool=`basename $0` + local pid="$$" + TMPDIR=`mktemp -d /tmp/${tool}.${pid}.XXXXX` \ + || die "Cannot make secure tmpdir" + fi +} + +# Sub: rm_tmpdir +# Remove the tmpdir and unset TMPDIR. +# +# Optional Global Variables: +# TMPDIR - TMPDIR set by . +# +# Set Global Variables: +# TMPDIR - Set to "". +rm_tmpdir() { + if [ -n "$TMPDIR" ] && [ -d "$TMPDIR" ]; then + rm -rf $TMPDIR + fi + TMPDIR="" +} + +# ########################################################################### +# End tmpdir package +# ########################################################################### diff --git a/t/lib/bash.t b/t/lib/bash.t new file mode 100644 index 00000000..aaf1fe28 --- /dev/null +++ b/t/lib/bash.t @@ -0,0 +1,23 @@ +#!/usr/bin/env perl + +BEGIN { + die "The PERCONA_TOOLKIT_BRANCH environment variable is not set.\n" + unless $ENV{PERCONA_TOOLKIT_BRANCH} && -d $ENV{PERCONA_TOOLKIT_BRANCH}; + unshift @INC, "$ENV{PERCONA_TOOLKIT_BRANCH}/lib"; +}; + +use strict; +use warnings FATAL => 'all'; +use English qw(-no_match_vars); + +use PerconaTest; + +my ($tool) = $PROGRAM_NAME =~ m/([\w-]+)\.t$/; +push @ARGV, "$trunk/t/lib/bash/*.sh" unless @ARGV; + +$ENV{LIB_DIR} = "$trunk/lib/bash"; +$ENV{T_LIB_DIR} = "$trunk/t/lib"; + +system("$trunk/util/test-bash-functions $trunk/t/lib/samples/bash/dummy.sh @ARGV"); + +exit; diff --git a/t/lib/bash/collect.sh b/t/lib/bash/collect.sh new file mode 100644 index 00000000..9d9249fa --- /dev/null +++ b/t/lib/bash/collect.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash + +TESTS=18 + +TMPFILE="$TEST_TMPDIR/parse-opts-output" +TMPDIR="$TEST_TMPDIR" +PATH="$PATH:$PERCONA_TOOLKIT_SANDBOX/bin" + +mkdir "$TMPDIR/collect" 2>/dev/null + +source "$LIB_DIR/log_warn_die.sh" +source "$LIB_DIR/parse_options.sh" +source "$LIB_DIR/safeguards.sh" +source "$LIB_DIR/alt_cmds.sh" +source "$LIB_DIR/collect.sh" + +parse_options "$T_LIB_DIR/samples/bash/po002.sh" --run-time 1 -- --defaults-file=/tmp/12345/my.sandbox.cnf + +# Prefix (with path) for the collect files. +local p="$TMPDIR/collect/2011_12_05" + +# Default collect, no extras like gdb, tcpdump, etc. +collect "$TMPDIR/collect" "2011_12_05" > $p-output 2>&1 + +# Even if this system doesn't have all the cmds, collect should still +# create all the default files. +ls -1 $TMPDIR/collect | sort > $TMPDIR/collect-files +no_diff \ + $TMPDIR/collect-files \ + $T_LIB_DIR/samples/bash/collect001.txt \ + "Default collect files" + +cmd_ok \ + "grep -q 'Avail' $p-df" \ + "df" + +# hostname is the last thing collected, so if it's ok, +# then the sub reached its end. +is \ + "`cat $p-hostname`" \ + "`hostname`" \ + "hostname" + +cmd_ok \ + "grep -q -i 'buffer pool' $p-innodbstatus1" \ + "innodbstatus1" + +cmd_ok \ + "grep -q -i 'buffer pool' $p-innodbstatus2" \ + "innodbstatus2" + +cmd_ok \ + "grep -q 'error log seems to be /tmp/12345/data/mysqld.log' $p-output" \ + "Finds MySQL error log" + +cmd_ok \ + "grep -q 'Status information:' $p-log_error" \ + "debug" + +cmd_ok \ + "grep -q 'COMMAND[ ]\+PID[ ]\+USER' $p-lsof" \ + "lsof" + +cmd_ok \ + "grep -q 'buf0buf.c' $p-mutex-status1" \ + "mutex-status1" + +cmd_ok \ + "grep -q 'buf0buf.c' $p-mutex-status2" \ + "mutex-status2" + +cmd_ok \ + "grep -q '^| Uptime' $p-mysqladmin" \ + "mysqladmin ext" + +cmd_ok \ + "grep -qP 'Database\tTable\tIn_use' $p-opentables1" \ + "opentables1" + +cmd_ok \ + "grep -qP 'Database\tTable\t\In_use' $p-opentables2" \ + "opentables2" + +cmd_ok \ + "grep -q '1. row' $p-processlist" \ + "processlist" + +cmd_ok \ + "grep -q 'mysqld' $p-ps" \ + "ps" + +cmd_ok \ + "grep -qP '^wait_timeout\t\d' $p-variables" \ + "variables" + +local iters=$(cat $p-df | grep -c '^TS ') +is "$iters" "1" "1 iteration/1s run time" + +# ########################################################################### +# Try longer run time. +# ########################################################################### + +parse_options "$T_LIB_DIR/samples/bash/po002.sh" --run-time 2 -- --defaults-file=/tmp/12345/my.sandbox.cnf + +rm $TMPDIR/collect/* + +collect "$TMPDIR/collect" "2011_12_05" > $p-output 2>&1 + +local iters=$(cat $p-df | grep -c '^TS ') +is "$iters" "2" "2 iteration/2s run time" + +# ############################################################################ +# Done +# ############################################################################ diff --git a/t/lib/bash/daemon.sh b/t/lib/bash/daemon.sh new file mode 100644 index 00000000..76ec4d0b --- /dev/null +++ b/t/lib/bash/daemon.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash + +TESTS=7 + +TMPDIR="$TEST_TMPDIR" +local file="$TMPDIR/pid-file" + +source "$LIB_DIR/log_warn_die.sh" +source "$LIB_DIR/daemon.sh" + +cmd_ok \ + "test ! -f $file" \ + "PID file doesn't exist" + +make_pid_file $file $$ + +cmd_ok \ + "test -f $file" \ + "PID file created" + +local pid=`cat $file` +is \ + "$pid" \ + "$$" \ + "Correct PID" + +remove_pid_file $file + +cmd_ok \ + "test ! -f $file" \ + "PID file removed" + +# ########################################################################### +# PID file already exists and proc is running. +# ########################################################################### +echo $$ > $file + +( + make_pid_file $file $$ >$TMPDIR/output 2>&1 +) + +cmd_ok \ + "grep -q \"PID file /tmp/percona-toolkit.test/pid-file already exists and its PID ($$) is running\" $TMPDIR/output" \ + "Does not overwrite PID file is PID is running" + +echo 999999 > $file + +make_pid_file $file $$ >$TMPDIR/output 2>&1 + +cmd_ok \ + "grep -q 'Overwriting PID file /tmp/percona-toolkit.test/pid-file because its PID (999999) is not running' $TMPDIR/output" \ + "Overwrites PID file if PID is not running" + +pid=`cat $file` +is \ + "$pid" \ + "$$" \ + "Correct PID" + +rm $file +rm $TMPDIR/output + +# ########################################################################### +# Done. +# ########################################################################### diff --git a/t/lib/bash/parse_options.sh b/t/lib/bash/parse_options.sh new file mode 100644 index 00000000..176c824c --- /dev/null +++ b/t/lib/bash/parse_options.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash + +TESTS=26 + +TMPFILE="$TEST_TMPDIR/parse-opts-output" + +source "$LIB_DIR/log_warn_die.sh" +source "$LIB_DIR/parse_options.sh" + +# ############################################################################ +# Parse options from POD using all default values. +# ############################################################################ + +TOOL="pt-stalk" +TMPDIR="$TEST_TMPDIR" +parse_options "$T_LIB_DIR/samples/bash/po001.sh" "" 2>$TMPFILE + +is "`cat $TMPFILE`" "" "No warnings or errors" + +is "$OPT_STRING_OPT" "" "Default string option" +is "$OPT_STRING_OPT2" "foo" "Default string option with default" +is "$OPT_TYPELESS_OPTION" "" "Default typeless option" +is "$OPT_NOPTION" "yes" "Defailt neg option" +is "$OPT_INT_OPT" "" "Default int option" +is "$OPT_INT_OPT2" "42" "Default int option with default" +is "$OPT_VERSION" "" "--version" + +# ############################################################################ +# Specify some opts, but use default values for the rest. +# ############################################################################ + +parse_options "$T_LIB_DIR/samples/bash/po001.sh" --int-opt 50 --typeless-option --string-opt bar + +is "$OPT_STRING_OPT" "bar" "Specified string option (spec)" +is "$OPT_STRING_OPT2" "foo" "Default string option with default (spec)" +is "$OPT_TYPELESS_OPTION" "yes" "Specified typeless option (spec)" +is "$OPT_NOPTION" "yes" "Default neg option (spec)" +is "$OPT_INT_OPT" "50" "Specified int option (spec)" +is "$OPT_INT_OPT2" "42" "Default int option with default (spec)" +is "$OPT_VERSION" "" "--version (spec)" + +# ############################################################################ +# Negate an option like --no-option. +# ############################################################################ + +parse_options "$T_LIB_DIR/samples/bash/po001.sh" --no-noption + +is "$OPT_STRING_OPT" "" "Default string option (neg)" +is "$OPT_STRING_OPT2" "foo" "Default string option with default (net)" +is "$OPT_TYPELESS_OPTION" "" "Default typeless option (neg)" +is "$OPT_NOPTION" "no" "Negated option (neg)" +is "$OPT_INT_OPT" "" "Default int option (neg)" +is "$OPT_INT_OPT2" "42" "Default int option with default (neg)" +is "$OPT_VERSION" "" "--version (neg)" + +# ############################################################################ +# Short form. +# ############################################################################ + +parse_options "$T_LIB_DIR/samples/bash/po001.sh" -v +is "$OPT_VERSION" "yes" "Short form" + +# ############################################################################ +# An unknown option should produce an error. +# ############################################################################ + +# Have to call this in a subshell because the error will cause an exit. +parse_options "$T_LIB_DIR/samples/bash/po001.sh" --foo >$TMPFILE 2>&1 +cmd_ok "grep -q 'Unknown option: --foo' $TMPFILE" "Error on unknown option" + +usage_or_errors "$T_LIB_DIR/samples/bash/po001.sh" >$TMPFILE 2>&1 +local err=$? +is "$err" "1" "Non-zero exit on unknown option" + +# ########################################################################### +# --help +# ########################################################################### +parse_options "$T_LIB_DIR/samples/bash/po001.sh" --help +usage_or_errors "$T_LIB_DIR/samples/bash/po001.sh" >$TMPFILE 2>&1 +no_diff \ + "$TMPFILE" \ + "$T_LIB_DIR/samples/bash/help001.txt" \ + "--help" + +# ############################################################################ +# Done +# ############################################################################ diff --git a/t/lib/bash/safeguards.sh b/t/lib/bash/safeguards.sh new file mode 100644 index 00000000..c874498f --- /dev/null +++ b/t/lib/bash/safeguards.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + +TESTS=11 + +source "$LIB_DIR/log_warn_die.sh" +source "$LIB_DIR/safeguards.sh" + +TMPDIR="$TEST_TMPDIR" +SAMPLE="$T_LIB_DIR/samples/bash" + +disk_space "/" > $TMPDIR/df-out +cmd_ok \ + "grep -q Avail $TMPDIR/df-out" \ + "disk_space()" + +is \ + "`wc -l $TMPDIR/df-out | awk '{print $1}'`" \ + "2" \ + "2-line df output" + +check_disk_space "$SAMPLE/diskspace001.txt" 22000 18 >$TMPDIR/out 2>&1 +is "$?" "0" "Enough disk space" +is \ + "`cat $TMPDIR/out`" \ + "" \ + "No output if enough disk space" + +check_disk_space "$SAMPLE/diskspace001.txt" 24000 18 >$TMPDIR/out 2>&1 +is "$?" "1" "Not enough MB free" +cmd_ok \ + "grep -q '19% free, 23487512 KB free; wanted more than 18% free or 24576000 KB free' $TMPDIR/out" \ + "Warning if not enough disk space" + +check_disk_space "$SAMPLE/diskspace001.txt" 22000 19 >$TMPDIR/out 2>&1 +is "$?" "1" "Not enough % free" + +# ########################################################################### +# Check with a margin (amount we plan to use in the future). +# ########################################################################### + +check_disk_space "$SAMPLE/diskspace001.txt" 22000 18 100 +is "$?" "0" "Enough disk space with margin" + +check_disk_space "$SAMPLE/diskspace001.txt" 23000 18 100 >$TMPDIR/out 2>&1 +is "$?" "1" "Not enough MB free with margin" + +check_disk_space "$SAMPLE/diskspace001.txt" 100 5 20000 >$TMPDIR/out 2>&1 +is "$?" "1" "Not enough % free with margin" +cmd_ok \ + "grep -q '3% free,' $TMPDIR/out" \ + "Calculates % free with margin" + +# ########################################################################### +# Done +# ########################################################################### diff --git a/t/lib/bash/tmpdir.sh b/t/lib/bash/tmpdir.sh new file mode 100644 index 00000000..55098f4d --- /dev/null +++ b/t/lib/bash/tmpdir.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +TESTS=9 + +source "$LIB_DIR/log_warn_die.sh" +source "$LIB_DIR/tmpdir.sh" + +is "$TMPDIR" "" "TMPDIR not defined" + +mk_tmpdir +cmd_ok "test -d $TMPDIR" "mk_tmpdir makes secure tmpdir" + +tmpdir=$TMPDIR; + +rm_tmpdir +cmd_ok "test ! -d $tmpdir" "rm_tmpdir" + +is "$TMPDIR" "" "rm_tmpdir resets TMPDIR" + +# ########################################################################### +# User-specified tmpdir. +# ########################################################################### + +local dir="/tmp/use--tmpdir" + +is "$TMPDIR" "" "TMPDIR not defined" + +cmd_ok "test ! -d $dir" "--tmpdir does not exist yet" + +mk_tmpdir $dir +is "$TMPDIR" "$dir" "mk_tmpdir uses --tmpdir" + +cmd_ok "test -d $dir" "mk_tmpdir creates --tmpdir" + +rm_tmpdir + +cmd_ok "test ! -d $tmpdir" "rm_tmpdir removes --tmpdir" + +# ########################################################################### +# Done +# ########################################################################### diff --git a/t/lib/samples/bash/collect001.txt b/t/lib/samples/bash/collect001.txt new file mode 100644 index 00000000..f76b8ecb --- /dev/null +++ b/t/lib/samples/bash/collect001.txt @@ -0,0 +1,33 @@ +2011_12_05-df +2011_12_05-disk-space +2011_12_05-diskstats +2011_12_05-hostname +2011_12_05-innodbstatus1 +2011_12_05-innodbstatus2 +2011_12_05-interrupts +2011_12_05-iostat +2011_12_05-iostat-overall +2011_12_05-log_error +2011_12_05-lsof +2011_12_05-meminfo +2011_12_05-mpstat +2011_12_05-mpstat-overall +2011_12_05-mutex-status1 +2011_12_05-mutex-status2 +2011_12_05-mysqladmin +2011_12_05-netstat +2011_12_05-netstat_s +2011_12_05-opentables1 +2011_12_05-opentables2 +2011_12_05-output +2011_12_05-processlist +2011_12_05-procstat +2011_12_05-procvmstat +2011_12_05-ps +2011_12_05-slabinfo +2011_12_05-stacktrace +2011_12_05-sysctl +2011_12_05-top +2011_12_05-variables +2011_12_05-vmstat +2011_12_05-vmstat-overall diff --git a/t/lib/samples/bash/diskspace001.txt b/t/lib/samples/bash/diskspace001.txt new file mode 100644 index 00000000..c68af8c9 --- /dev/null +++ b/t/lib/samples/bash/diskspace001.txt @@ -0,0 +1,2 @@ +Filesystem 1024-blocks Used Available Capacity Mounted on +/dev/disk0s2 118153176 94409664 23487512 81% / diff --git a/t/lib/samples/bash/dummy.sh b/t/lib/samples/bash/dummy.sh new file mode 100644 index 00000000..a7eea1ee --- /dev/null +++ b/t/lib/samples/bash/dummy.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +# This is a dummy script for testing the Bash libs. t/lib/bashLibs.t +# calls "util/test-bash-functions dummy.sh ". diff --git a/t/lib/samples/bash/help001.txt b/t/lib/samples/bash/help001.txt new file mode 100644 index 00000000..65dc71b2 --- /dev/null +++ b/t/lib/samples/bash/help001.txt @@ -0,0 +1,30 @@ +Usage: pt-stalk [OPTIONS] [-- MYSQL_OPTIONS] + +For more information, 'man pt-stalk' or 'perldoc /Users/daniel/p/bash-tool-libs/t/lib/samples/bash/po001.sh'. + +Command line options: + +--help + Print help and exit. + +--int-opt + Int option without a default. + +--int-opt2 + Int option with a default. + +--noption + Negatable option. + +--string-opt + String option without a default. + +--string-opt2 + String option with a default. + +--typeless-option + Just an option. + +--version + Print tool's version and exit. + diff --git a/t/lib/samples/bash/po001.sh b/t/lib/samples/bash/po001.sh new file mode 100644 index 00000000..15b71d25 --- /dev/null +++ b/t/lib/samples/bash/po001.sh @@ -0,0 +1,308 @@ +#!/usr/bin/env bash + +# This is a fake script for testing the parse_options.sh lib. +exit 0; + +# ############################################################################ +# Documentation +# ############################################################################ +:<<'DOCUMENTATION' +=pod + +=head1 NAME + +pt-stalk - Wait for a condition to occur then begin collecting data. + +=head1 SYNOPSIS + +Usage: pt-stalk [OPTIONS] [-- MYSQL_OPTIONS] + +pt-stalk watches for a condition to become true, and when it does, executes +a script. By default it executes L, but that can be customized. +This tool is useful for gathering diagnostic data when an infrequent event +occurs, so an expert person can review the data later. + +=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. + +pt-stalk is a read-only tool. It should be very low-risk. + +At the time of this release, we know of no bugs that could cause serious harm +to users. + +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<"BUGS"> for more information on filing bugs and getting help. + +=head1 DESCRIPTION + +Although pt-stalk comes pre-configured to do a specific thing, in general +this tool is just a skeleton script for the following flow of actions: + +=over + +=item 1. + +Loop infinitely, sleeping between iterations. + +=item 2. + +In each iteration, run some command and get the output. + +=item 3. + +If the command fails or the output is larger than the threshold, +execute the collection script; but do not execute if the destination disk +is too full. + +=back + +By default, the tool is configured to execute mysqladmin extended-status and +extract the value of the Threads_connected variable; if this is greater than +100, it runs the collection script. This is really just placeholder code, +and almost certainly needs to be customized! + +If the tool does execute the collection script, it will wait for a while +before checking and executing again. This is to prevent a continuous +condition from causing a huge number of executions to fire off. + +The name 'stalk' is because 'watch' is already taken, and 'stalk' is fun. + +=head1 CONFIGURING + +If the file F exists in the current working directory, then +L<"ENVIRONMENT"> variables are imported from it. For example, the config +file has the format: + + INTERVAL=10 + GDB=yes + +See L<"ENVIRONMENT">. + +=head1 OPTIONS + +=over + +=item --string-opt + +type: string + +String option without a default. + +=item --string-opt2 + +type: string; default: foo + +String option with a default. + +=item --typeless-option + +Just an option. + +=item --noption + +default: yes; negatable: yes + +Negatable option. + +=item --int-opt + +type: int + +Int option without a default. + +=item --int-opt2 + +type: int; default: 42 + +Int option with a default. + +=item --version + +short form: -v + +Print tool's version and exit. + +=item --help + +Print help and exit. + +=back + +=head1 ENVIRONMENT + +The following environment variables configure how, what, and when the tool +runs. They are all optional and can be specified either on the command line +or in the F config file (see L<"CONFIGURING">). + +=over + +=item THRESHOLD (default 100) + +This is the max number of we want to tolerate. + +=item VARIABLE (default Threads_connected} + +This is the thing to check for. + +=item CYCLES (default 1) + +How many times must the condition be met before the script will fire? + +=item GDB (default no) + +Collect GDB stacktraces? + +=item OPROFILE (default yes) + +Collect oprofile data? + +=item STRACE (default no) + +Collect strace data? + +=item TCPDUMP (default yes) + +Collect tcpdump data? + +=item EMAIL + +Send mail to this list of addresses when the script triggers. + +=item MYSQLOPTIONS + +Any options to pass to mysql/mysqladmin, such as -u, -p, etc + +=item INTERVAL (default 30) + +This is the interval between checks. + +=item MAYBE_EMPTY (default no) + +If the command you're running to detect the condition is allowed to return +nothing (e.g. a grep line that might not even exist if there's no problem), +then set this to "yes". + +=item COLLECT (default ${HOME}/bin/pt-collect) + +This is the location of the 'collect' script. + +=item DEST (default ${HOME}/collected/) + +This is where to store the collected data. + +=item DURATION (default 30) + +How long to collect statistics data for? Make sure that this isn't longer +than SLEEP. + +=item SLEEP (default DURATION * 10) + +How long to sleep after collecting? + +=item PCT_THRESHOLD (default 95) + +Bail out if the disk is more than this %full. + +=item MB_THRESHOLD (default 100) + +Bail out if the disk has less than this many MB free. + +=item PURGE (default 30) + +Remove samples after this many days. + +=back + +=head1 SYSTEM REQUIREMENTS + +This tool requires Bash v3 or newer. + +=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, Justin Swanhart, and Fernando Ipar + +=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 2010-2011 Baron Schwartz, 2011 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-stalk 1.0.1 + +=cut + +DOCUMENTATION diff --git a/t/lib/samples/bash/po002.sh b/t/lib/samples/bash/po002.sh new file mode 100644 index 00000000..63a8672b --- /dev/null +++ b/t/lib/samples/bash/po002.sh @@ -0,0 +1,212 @@ +#!/usr/bin/env bash + +: + +# ############################################################################ +# Documentation +# ############################################################################ +:<<'DOCUMENTATION' +=pod + +=head1 NAME + +pt-stalk - Wait for a condition to occur then begin collecting data. + +=head1 OPTIONS + +=over + +=item --collect + +default: yes; negatable: yes + +Collect system information. + +=item --collect-gdb + +Collect GDB stacktraces. + +=item --collect-oprofile + +Collect oprofile data. + +=item --collect-strace + +Collect strace data. + +=item --collect-tcpdump + +Collect tcpdump data. + +=item --cycles + +type: int; default: 5 + +Number of times condition must be met before triggering collection. + +=item --daemonize + +default: yes; negatable: yes + +Daemonize the tool. + +=item --dest + +type: string + +Where to store collected data. + +=item --disk-byte-limit + +type: int; default: 100 + +Exit if the disk has less than this many MB free. + +=item --disk-pct-limit + +type: int; default: 5 + +Exit if the disk is less than this %full. + +=item --execute-command + +type: string; default: pt-collect + +Location of the C tool. + +=item --function + +type: string; default: status + +Built-in function name or plugin file name which returns the value of C. + +Possible values are: + +=over + +=item * status + +Grep the value of C from C. + +=item * processlist + +Count the number of processes in C whose +C column matches C. For example: + + TRIGGER_FUNCTION="processlist" \ + VARIABLE="State" \ + MATCH="statistics" \ + THRESHOLD="10" + +The above triggers when more than 10 processes are in the "statistics" state. +C must be specified for this trigger function. + +=item * magic + +TODO + +=item * plugin file name + +A plugin file allows you to specify a custom trigger function. The plugin +file must contain a function called C. For example: + + trg_plugin() { + # Do some stuff. + echo "$value" + } + +The last output if the function (its "return value") must be a number. +This number is compared to C. All L<"ENVIRONMENT"> variables +are available to the function. + +Do not alter the tool's existing global variables. Prefix any plugin-specific +global variables with "PLUGIN_". + +=back + +=item --help + +Print help and exit. + +=item --interval + +type: int; default: 1 + +Interval between checks. + +=item --iterations + +type: int + +Exit after triggering C this many times. By default, the tool +will collect as many times as it's triggered. + +=item --log + +type: string; default: /var/log/pt-stalk.log + +Print all output to this file when daemonized. + +=item --match + +type: string + +Match pattern for C L<"--function">. + +=item --notify-by-email + +type: string + +Send mail to this list of addresses when C triggers. + +=item --pid FILE + +type: string; default: /var/run/pt-stalk.pid + +Create a PID file when daemonized. + +=item --retention-time + +type: int; default: 30 + +Remove samples after this many days. + +=item --run-time + +type: int; default: 30 + +How long to collect statistics data for? + +Make sure that this isn't longer than SLEEP. + +=item --sleep + +type: int; default: 300 + +How long to sleep after collecting? + +=item --threshold N + +type: int; default: 25 + +Max number of C to tolerate. + +=item --variable NAME + +type: string; default: Threads_running + +This is the thing to check for. + +=item --version + +Print tool's version and exit. + +=back + +=head1 ENVIRONMENT + +No env vars used. + +=cut + +DOCUMENTATION diff --git a/util/test-bash-functions b/util/test-bash-functions index 1ffb0075..1a14a0de 100755 --- a/util/test-bash-functions +++ b/util/test-bash-functions @@ -34,9 +34,10 @@ die() { # Paths # ############################################################################ -TMPDIR="/tmp/percona-toolkit.test" -if [ ! -d $TMPDIR ]; then - mkdir $TMPDIR +# Do not use TMPDIR because the tools use it for their own secure tmpdir. +TEST_TMPDIR="/tmp/percona-toolkit.test" +if [ ! -d $TEST_TMPDIR ]; then + mkdir $TEST_TMPDIR fi # ############################################################################ @@ -47,6 +48,7 @@ fi load_tests() { local test_files="$@" local i=0 + local n_tests=0 for t in $test_files; do # Return unless the test file is bash. There may be other types of # files in the tool's test dir. @@ -56,22 +58,22 @@ load_tests() { head -n 1 $t | grep -q bash || continue tests[$i]=$t + i=$((i + 1)) + number_of_tests=$(grep --max-count 1 '^TESTS=[0-9]' $t | cut -d'=' -f2) if [ -z "$number_of_tests" ]; then - i=$(( i + 1 )) + n_tests=$(( $n_tests + 1 )) else - i=$(( i + $number_of_tests )) + n_tests=$(( $n_tests + $number_of_tests )) fi done - echo "1..$i" + echo "1..$n_tests" } # Source a test file to run whatever it contains (hopefully tests!). run_test() { local t=$1 # test file name, e.g. "group-by-all-01" for pt-diskstats - rm -rf $TMPDIR/* >/dev/null 2>&1 - - TEST_NUMBER=1 # test number in this test file + rm -rf $TEST_TMPDIR/* >/dev/null 2>&1 # Tests assume that they're being ran from their own dir, so they access # sample files like "samples/foo.txt". So cd to the dir of the test file @@ -91,19 +93,18 @@ run_test() { # Print a TAP-style test result. result() { local result=$1 - local test_name=${TEST_NAME:-"$TEST_NUMBER"} + local test_name=$2 if [ $result -eq 0 ]; then echo "ok $testno - $TEST_FILE $test_name" else echo "not ok $testno - $TEST_FILE $test_name" failed_tests=$(( failed_tests + 1)) echo "# Failed '$test_command'" >&2 - if [ -f $TMPDIR/failed_result ]; then - cat $TMPDIR/failed_result | sed -e 's/^/# /' -e '30q' >&2 + if [ -f $TEST_TMPDIR/failed_result ]; then + cat $TEST_TMPDIR/failed_result | sed -e 's/^/# /' -e '30q' >&2 fi fi testno=$((testno + 1)) - TEST_NUMBER=$((TEST_NUMBER + 1)) return $result } @@ -114,17 +115,26 @@ result() { no_diff() { local got=$1 local expected=$2 + local test_name=$3 test_command="diff $got $expected" - eval $test_command > $TMPDIR/failed_result 2>&1 - result $? + eval $test_command > $TEST_TMPDIR/failed_result 2>&1 + result $? "$test_name" } is() { local got=$1 local expected=$2 + local test_name=$3 test_command="\"$got\" == \"$expected\"" test "$got" = "$expected" - result $? + result $? "$test_name" +} + +cmd_ok() { + local test_command=$1 + local test_name=$2 + eval $test_command + result $? "$test_name" } # ############################################################################ @@ -157,6 +167,6 @@ for t in "${tests[@]}"; do run_test $t done -rm -rf $TMPDIR +rm -rf $TEST_TMPDIR exit $failed_tests diff --git a/util/update-modules b/util/update-modules index fb69fcb7..b976e07e 100755 --- a/util/update-modules +++ b/util/update-modules @@ -49,7 +49,11 @@ file_is_modified() { pkgs_in_tool() { local tool=$1 - pkgs=$(grep '^package [A-Za-z]*;' $tool | cut -d' ' -f2 | cut -d';' -f1) + if [ "$tool_lang" = "perl" ]; then + pkgs=$(grep '^package [A-Za-z]*;' $tool | cut -d' ' -f2 | cut -d';' -f1) + else + pkgs=$(grep '^# [a-z_]* package' $tool | awk '{print $2}') + fi } replace_pkg_in_tool() { @@ -72,7 +76,8 @@ replace_pkg_in_tool() { head -n $pkg_start_line $tool_file > $tmp_file - echo "# $pkg package + if [ "$tool_lang" = "perl" ]; then + echo "# $pkg 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/$pkg.pm @@ -80,11 +85,24 @@ replace_pkg_in_tool() { # See https://launchpad.net/percona-toolkit for more information. # ########################################################################### {" >> $tmp_file + else + echo "# $pkg 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/bash/$pkg.sh +# t/lib/bash/$pkg.sh +# See https://launchpad.net/percona-toolkit for more information. +# ########################################################################### +" >> $tmp_file + fi $BRANCH/util/extract-package $pkg $pkg_file | grep -v '^ *#' >> $tmp_file - echo "} -# ########################################################################### + if [ "$tool_lang" = "perl" ]; then + echo "}" + fi + +echo "# ########################################################################### # End $pkg package" >> $tmp_file tail -n +$pkg_end_line $tool_file >> $tmp_file @@ -97,14 +115,19 @@ replace_pkg_in_tool() { # ############################################################################ tool_file=$1 + if [ -z "$tool_file" ]; then die "Usage: $0 TOOL [MODULE...]" fi + if [ ! -f $tool_file ]; then die "$tool_file does not exist" fi -if [ -z "$(head -n 1 $tool_file | grep perl)" ]; then - die "$tool_file is not a Perl tool" + +if [ -n "$(head -n 1 $tool_file | grep perl)" ]; then + tool_lang="perl" +else + tool_lang="bash" fi tool=$(basename $tool_file) @@ -124,11 +147,17 @@ for pkg in $pkgs; do continue fi - pkg_file="$BRANCH/lib/$pkg.pm" + if [ "$tool_lang" = "perl" ]; then + pkg_file="$BRANCH/lib/$pkg.pm" + else + pkg_file="$BRANCH/lib/bash/$pkg.sh" + fi + if [ ! -f $pkg_file ]; then warn "$pkg_file does not exist" continue fi + if file_is_modified $pkg_file; then warn "$pkg_file has uncommitted changes" continue