merge changes back from Daniel

This commit is contained in:
baron@percona.com
2012-01-26 16:05:50 -05:00
15 changed files with 924 additions and 569 deletions

View File

@@ -49,48 +49,83 @@ die() {
# ###########################################################################
set -u
ARGV="" # Non-option args (probably input files)
EXT_ARGV="" # Everything after -- (args for an external command)
HAVE_EXT_ARGV="" # Got --, everything else is put into EXT_ARGV
OPT_ERRS=0 # How many command line option errors
OPT_VERSION="no" # If --version was specified
OPT_HELP="no" # If --help was specified
OPT_VERSION="" # If --version was specified
OPT_HELP="" # If --help was specified
PO_DIR="$TMPDIR/po" # Directory with program option spec files
usage() {
local file="$1"
local usage=$(grep '^Usage: ' "$file")
echo $usage >&2
echo >&2
echo "For more information, 'man $TOOL' or 'perldoc $file'." >&2
echo $usage
echo
echo "For more information, 'man $TOOL' or 'perldoc $file'."
}
usage_or_errors() {
local file="$1"
if [ "$OPT_VERSION" = "yes" ]; then
if [ "$OPT_VERSION" ]; then
local version=$(grep '^pt-[^ ]\+ [0-9]' "$file")
echo "$version"
return 1
fi
if [ "$OPT_HELP" = "yes" ]; then
if [ "$OPT_HELP" ]; 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
echo
echo "Command line options:"
echo
perl -e '
use strict;
use warnings FATAL => qw(all);
my $lcol = 20; # Allow this much space for option names.
my $rcol = 80 - $lcol; # The terminal is assumed to be 80 chars wide.
my $name;
while ( <> ) {
my $line = $_;
chomp $line;
if ( $line =~ s/^long:/ --/ ) {
$name = $line;
}
elsif ( $line =~ s/^desc:// ) {
$line =~ s/ +$//mg;
my @lines = grep { $_ }
$line =~ m/(.{0,$rcol})(?:\s+|\Z)/g;
if ( length($name) >= $lcol ) {
print $name, "\n", (q{ } x $lcol);
}
else {
printf "%-${lcol}s", $name;
}
print join("\n" . (q{ } x $lcol), @lines);
print "\n";
}
}
' "$PO_DIR"/*
echo
echo "Options and values after processing arguments:"
echo
for opt in $(ls "$PO_DIR"); do
local varname="OPT_$(echo "$opt" | tr a-z- A-Z_)"
local varvalue="${!varname}"
printf -- " --%-30s %s" "$opt" "${varvalue:-(No value)}"
echo
done
return 1
fi
if [ $OPT_ERRS -gt 0 ]; then
echo >&2
echo
usage "$file"
return 1
fi
@@ -102,60 +137,88 @@ parse_options() {
local file="$1"
shift
if [ ! -d "$TMPDIR/po/" ]; then
mkdir "$TMPDIR/po/"
ARGV=""
EXT_ARGV=""
HAVE_EXT_ARGV=""
OPT_ERRS=0
OPT_VERSION=""
OPT_HELP=""
PO_DIR="$TMPDIR/po"
if [ ! -d "$PO_DIR" ]; then
mkdir "$PO_DIR"
if [ $? -ne 0 ]; then
echo "Cannot mkdir $TMPDIR/po/" >&2
echo "Cannot mkdir $PO_DIR" >&2
exit 1
fi
fi
rm -rf "$TMPDIR"/po/*
rm -rf "$PO_DIR"/*
if [ $? -ne 0 ]; then
echo "Cannot rm -rf $TMPDIR/po/*" >&2
echo "Cannot rm -rf $PO_DIR/*" >&2
exit 1
fi
(
export PO_DIR="$TMPDIR/po"
cat "$file" | perl -ne '
BEGIN { $/ = ""; }
next unless $_ =~ m/^=head1 OPTIONS/;
while ( defined(my $para = <>) ) {
last if $para =~ m/^=head1/;
_parse_pod "$file" # Parse POD into program option (po) spec files
_eval_po # Eval po into existence with default values
if [ $# -ge 2 ] && [ "$1" = "--config" ]; then
shift # --config
local user_config_files="$1"
shift # that ^
local IFS=","
for user_config_file in $user_config_files; do
_parse_config_files "$user_config_file"
done
else
_parse_config_files "/etc/percona-toolkit/percona-toolkit.conf" "/etc/percona-toolkit/$TOOL.conf" "$HOME/.percona-toolkit.conf" "$HOME/.$TOOL.conf"
fi
_parse_command_line "$@"
}
_parse_pod() {
local file="$1"
cat "$file" | PO_DIR="$PO_DIR" 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: $!";
print $opt_fh "long:$opt\n";
$para = <>;
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: $!";
print $opt_fh "long:$opt\n";
if ( $para =~ m/^[a-z ]+:/ ) {
map {
chomp;
my ($attrib, $val) = split(/: /, $_);
print $opt_fh "$attrib:$val\n";
} split(/; /, $para);
$para = <>;
chomp;
if ( $para =~ m/^[a-z ]+:/ ) {
map {
chomp;
my ($attrib, $val) = split(/: /, $_);
print $opt_fh "$attrib:$val\n";
} split(/; /, $para);
$para = <>;
chomp;
}
my ($desc) = $para =~ m/^([^?.]+)/;
print $opt_fh "desc:$desc.\n";
close $opt_fh;
}
my ($desc) = $para =~ m/^([^?.]+)/;
print $opt_fh "desc:$desc.\n";
close $opt_fh;
}
last;
'
)
}
last;
'
}
for opt_spec in $(ls "$TMPDIR/po/"); do
_eval_po() {
local IFS=":"
for opt_spec in "$PO_DIR"/*; 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)
local size=0
while read key val; do
case "$key" in
long)
opt=$(echo $val | sed 's/-/_/g' | tr [:lower:] [:upper:])
@@ -166,6 +229,7 @@ parse_options() {
"short form")
;;
type)
[ "$val" = "size" ] && size=1
;;
desc)
;;
@@ -175,13 +239,13 @@ parse_options() {
fi
;;
*)
echo "Invalid attribute in $TMPDIR/po/$opt_spec: $line" >&2
echo "Invalid attribute in $opt_spec: $line" >&2
exit 1
esac
done < "$TMPDIR/po/$opt_spec"
done < "$opt_spec"
if [ -z "$opt" ]; then
echo "No long attribute in option spec $TMPDIR/po/$opt_spec" >&2
echo "No long attribute in option spec $opt_spec" >&2
exit 1
fi
@@ -192,74 +256,157 @@ parse_options() {
fi
fi
if [ $size -eq 1 -a -n "$default_val" ]; then
default_val=$(size_to_bytes $default_val)
fi
eval "OPT_${opt}"="$default_val"
done
}
for opt; do
if [ $# -eq 0 ]; then
break # no more opts
_parse_config_files() {
for config_file in "$@"; do
test -f "$config_file" || continue
while read config_opt; do
echo "$config_opt" | grep '^[ ]*[^#]' >/dev/null 2>&1 || continue
config_opt="$(echo "$config_opt" | sed -e 's/^[ ]*//' -e 's/[ ]*\$//' -e 's/[ ]*=[ ]*/=/' -e 's/[ ]*#.*$//')"
[ "$config_opt" = "" ] && continue
if ! [ "$HAVE_EXT_ARGV" ]; then
config_opt="--$config_opt"
fi
_parse_command_line "$config_opt"
done < "$config_file"
HAVE_EXT_ARGV="" # reset for each file
done
}
_parse_command_line() {
local opt=""
local val=""
local next_opt_is_val=""
local opt_is_ok=""
local opt_is_negated=""
local real_opt=""
local required_arg=""
local spec=""
for opt in "$@"; do
if [ "$opt" = "--" -o "$opt" = "----" ]; then
HAVE_EXT_ARGV=1
continue
fi
opt=$1
if [ "$opt" = "--" ]; then
shift
EXT_ARGV="$@"
break
fi
shift
if [ $(expr "$opt" : "-") -eq 0 ]; then
if [ -z "$ARGV" ]; then
ARGV="$opt"
if [ "$HAVE_EXT_ARGV" ]; then
if [ "$EXT_ARGV" ]; then
EXT_ARGV="$EXT_ARGV $opt"
else
ARGV="$ARGV $opt"
EXT_ARGV="$opt"
fi
continue
fi
local real_opt="$opt"
if $(echo $opt | grep -q '^--no-'); then
neg=1
opt=$(echo $opt | sed 's/^--no-//')
else
neg=0
opt=$(echo $opt | sed 's/^-*//')
fi
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
local required_arg=$(cat $spec | awk -F: '/^type:/{print $2}')
if [ -n "$required_arg" ]; then
if [ $# -eq 0 ]; then
if [ "$next_opt_is_val" ]; then
next_opt_is_val=""
if [ $# -eq 0 ] || [ $(expr "$opt" : "-") -eq 1 ]; then
OPT_ERRS=$(($OPT_ERRS + 1))
echo "$real_opt requires a $required_arg argument" >&2
continue
else
val="$1"
shift
fi
val="$opt"
opt_is_ok=1
else
if [ $neg -eq 0 ]; then
val="yes"
if [ $(expr "$opt" : "-") -eq 0 ]; then
if [ -z "$ARGV" ]; then
ARGV="$opt"
else
ARGV="$ARGV $opt"
fi
continue
fi
real_opt="$opt"
if $(echo $opt | grep '^--no-' >/dev/null); then
opt_is_negated=1
opt=$(echo $opt | sed 's/^--no-//')
else
val="no"
opt_is_negated=""
opt=$(echo $opt | sed 's/^-*//')
fi
if $(echo $opt | grep '^[a-z-][a-z-]*=' >/dev/null 2>&1); then
val="$(echo $opt | awk -F= '{print $2}')"
opt="$(echo $opt | awk -F= '{print $1}')"
fi
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
required_arg=$(cat "$spec" | awk -F: '/^type:/{print $2}')
if [ "$required_arg" ]; then
if [ "$val" ]; then
opt_is_ok=1
else
next_opt_is_val=1
fi
else
if [ "$val" ]; then
OPT_ERRS=$(($OPT_ERRS + 1))
echo "Option $real_opt does not take a value" >&2
continue
fi
if [ "$opt_is_negated" ]; then
val=""
else
val="yes"
fi
opt_is_ok=1
fi
fi
opt=$(cat $spec | grep '^long:' | cut -d':' -f2 | sed 's/-/_/g' | tr [:lower:] [:upper:])
if [ "$opt_is_ok" ]; then
opt=$(cat "$spec" | grep '^long:' | cut -d':' -f2 | sed 's/-/_/g' | tr [:lower:] [:upper:])
eval "OPT_$opt"="$val"
if grep "^type:size" "$spec" >/dev/null; then
val=$(size_to_bytes $val)
fi
eval "OPT_$opt"="'$val'"
opt=""
val=""
next_opt_is_val=""
opt_is_ok=""
opt_is_negated=""
real_opt=""
required_arg=""
spec=""
fi
done
}
size_to_bytes() {
local size="$1"
echo $size | perl -ne '%f=(B=>1, K=>1_024, M=>1_048_576, G=>1_073_741_824, T=>1_099_511_627_776); m/^(\d+)([kMGT])?/i; print $1 * $f{uc($2 || "B")};'
}
# ###########################################################################
# End parse_options package
# ###########################################################################
@@ -345,33 +492,37 @@ disk_space() {
check_disk_space() {
local file="$1"
local mb="${2:-0}"
local pc="${3:-0}"
local mb_margin="${4:-0}"
local min_free_bytes="${2:-0}"
local min_free_pct="${3:-0}"
local bytes_margin="${4:-0}"
local kb=$(($mb * 1024))
local kb_margin=$(($mb_margin * 1024))
local used_bytes=$(cat "$file" | awk '/^\//{print $3 * 1024}');
local free_bytes=$(cat "$file" | awk '/^\//{print $4 * 1024}');
local pct_used=$(cat "$file" | awk '/^\//{print $5}' | sed -e 's/%//g');
local pct_free=$((100 - $pct_used))
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');
local real_free_bytes=$free_bytes
local real_pct_free=$pct_free
if [ "$kb_margin" -gt "0" ]; then
local kb_total=$(($kb_used + $kb_free))
if [ $bytes_margin -gt 0 ]; then
used_bytes=$(($used_bytes + $bytes_margin))
free_bytes=$(($free_bytes - $bytes_margin))
pct_used=$(awk "BEGIN { printf(\"%d\", ($used_bytes/($used_bytes + $free_bytes)) * 100) }")
kb_used=$(($kb_used + $kb_margin))
kb_free=$(($kb_free - $kb_margin))
pc_used=$(awk "BEGIN { printf(\"%d\", $kb_used/$kb_total * 100) }")
pct_free=$((100 - $pct_used))
fi
local pc_free=$((100 - $pc_used))
if [ $free_bytes -lt $min_free_bytes -o $pct_free -lt $min_free_pct ]; then
warn "Not enough free disk space:
Limit: ${min_free_pct}% free, ${min_free_bytes} bytes free
Actual: ${real_pct_free}% free, ${real_free_bytes} bytes free (- $bytes_margin bytes margin)
"
cat "$file" >&2
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
return 1 # not enough disk space
fi
return 0
return 0 # disk space is OK
}
# ###########################################################################
@@ -471,7 +622,7 @@ collect() {
fi
fi
if [ "$CMD_GDB" -a "$OPT_COLLECT_GDB" = "yes" -a "$mysqld_pid" ]; then
if [ "$CMD_GDB" -a "$OPT_COLLECT_GDB" -a "$mysqld_pid" ]; then
$CMD_GDB \
-ex "set pagination 0" \
-ex "thread apply all bt" \
@@ -510,7 +661,7 @@ collect() {
open_tables >> "$d/$p-opentables1" 2>&1 &
local tcpdump_pid=""
if [ "$CMD_TCPDUMP" -a "$OPT_COLLECT_TCPDUMP" = "yes" ]; then
if [ "$CMD_TCPDUMP" -a "$OPT_COLLECT_TCPDUMP" ]; 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} &
@@ -519,12 +670,12 @@ collect() {
fi
local have_oprofile="no"
if [ "$CMD_OPCONTROL" -a "$OPT_COLLECT_OPROFILE" = "yes" ]; then
if [ "$CMD_OPCONTROL" -a "$OPT_COLLECT_OPROFILE" ]; then
if $CMD_OPCONTROL --init; then
$CMD_OPCONTROL --start --no-vmlinux
have_oprofile="yes"
fi
elif [ "$CMD_STRACE" -a "$OPT_COLLECT_STRACE" = "yes" ]; then
elif [ "$CMD_STRACE" -a "$OPT_COLLECT_STRACE" ]; then
$CMD_STRACE -T -s 0 -f -p $mysqld_pid > "${DEST}/$d-strace" 2>&1 &
local strace_pid=$!
fi
@@ -561,8 +712,8 @@ collect() {
disk_space $d > $d/$p-disk-space
check_disk_space \
$d/$p-disk-space \
"$OPT_DISK_BYTE_LIMIT" \
"$OPT_DISK_PCT_LIMIT" \
"$OPT_DISK_BYTES_FREE" \
"$OPT_DISK_PCT_FREE" \
|| break
sleep $(date +%s.%N | awk '{print 1 - ($1 % 1)}')
@@ -623,7 +774,7 @@ collect() {
"/path/to/mysqld'" \
> "$d/$p-opreport"
fi
elif [ "$CMD_STRACE" -a "$OPT_COLLECT_STRACE" = "yes" ]; then
elif [ "$CMD_STRACE" -a "$OPT_COLLECT_STRACE" ]; then
kill -s 2 $strace_pid
sleep 1
kill -s 15 $strace_pid
@@ -770,11 +921,6 @@ trg_processlist() {
return
}
trg_magic() {
echo "TODO"
return
}
oktorun() {
if [ $OKTORUN -eq 0 ]; then
EXIT_REASON="OKTORUN is false"
@@ -828,7 +974,7 @@ sigtrap() {
stalk() {
local cycles_true=0 # increment each time check is true, else set to 0
local matched="no" # set to "yes" when check is true
local matched="" # set to "yes" when check is true
local last_prefix="" # prefix of last collection
while oktorun; do
@@ -841,61 +987,65 @@ stalk() {
if [ -z "$value" ]; then
# No value. Maybe we failed to connect to MySQL?
warn "Detected value is empty; something failed? Trigger exit status: $trg_exit_status"
matched="no"
matched=""
cycles_true=0
elif [ $value -gt $OPT_THRESHOLD ]; then
matched="yes"
cycles_true=$(($cycles_true + 1))
else
matched="no"
matched=""
cycles_true=0
fi
local msg="Check results: $OPT_VARIABLE=$value, matched=$matched, cycles_true=$cycles_true"
local msg="Check results: $OPT_VARIABLE=$value, matched=${matched:-no}, cycles_true=$cycles_true"
log "$msg"
if [ "$matched" = "yes" -a $cycles_true -ge $OPT_CYCLES ]; then
if [ "$matched" -a $cycles_true -ge $OPT_CYCLES ]; then
# ##################################################################
# Start collecting, maybe.
# ##################################################################
local prefix="${OPT_PREFIX:-$(date +%F-%T | tr :- _)}"
log "Collect triggered"
# Check if we'll have enough disk space to collect. Disk space
# is also checked every interval while collecting.
local margin="20" # default 20M margin, unless:
if [ -n "$last_prefix" ]; then
margin=$(du -mc "$OPT_DEST"/"$last_prefix"-* | tail -n 1 | awk '{print $1'})
fi
disk_space "$OPT_DEST" > "$OPT_DEST/$prefix-disk-space"
check_disk_space \
"$OPT_DEST/$prefix-disk-space" \
"$OPT_DISK_BYTE_LIMIT" \
"$OPT_DISK_PCT_LIMIT" \
"$margin" # real used MB + margin MB
if [ $? -eq 0 ]; then
# There should be enough disk space, so collect.
log "$msg" >> "$OPT_DEST/$prefix-trigger"
log "pt-stalk ran with $RAN_WITH" >> "$OPT_DEST/$prefix-trigger"
last_prefix="$prefix"
# Send email to whomever that collect has been triggered.
if [ "$OPT_NOTIFY_BY_EMAIL" ]; then
echo "$msg on $(hostname)" \
| mail -s "Collect triggered on $(hostname)" \
"$OPT_NOTIFY_BY_EMAIL"
fi
# Send email to whomever that collect has been triggered.
if [ "$OPT_NOTIFY_BY_EMAIL" ]; then
echo "$msg on $(hostname)" \
| mail -s "Collect triggered on $(hostname)" \
"$OPT_NOTIFY_BY_EMAIL"
if [ "$OPT_COLLECT" ]; then
local prefix="${OPT_PREFIX:-$(date +%F-%T | tr :- _)}"
# Check if we'll have enough disk space to collect. Disk space
# is also checked every interval while collecting.
local margin="20971520" # default 20M margin, unless:
if [ -n "$last_prefix" ]; then
margin=$(du -mc "$OPT_DEST"/"$last_prefix"-* | tail -n 1 | awk '{print $1'})
fi
disk_space "$OPT_DEST" > "$OPT_DEST/$prefix-disk-space"
check_disk_space \
"$OPT_DEST/$prefix-disk-space" \
"$OPT_DISK_BYTES_FREE" \
"$OPT_DISK_PCT_FREE" \
"$margin"
if [ $? -eq 0 ]; then
# There should be enough disk space, so collect.
log "$msg" >> "$OPT_DEST/$prefix-trigger"
log "pt-stalk ran with $RAN_WITH" >> "$OPT_DEST/$prefix-trigger"
last_prefix="$prefix"
# Fork and background the collect subroutine which will
# run for --run-time seconds. We (the parent) sleep
# while its collecting (hopefully --sleep is longer than
# --run-time).
(
collect "$OPT_DEST" "$prefix"
) >> "$OPT_DEST/$prefix-output" 2>&1 &
else
# There will not be enough disk space, so do not collect.
warn "Collect canceled because there will not be enough disk space after collecting another $margin MB"
fi
# Fork and background the collect subroutine which will
# run for --run-time seconds. We (the parent) sleep
# while its collecting (hopefully --sleep is longer than
# --run-time).
(
collect "$OPT_DEST" "$prefix"
) >> "$OPT_DEST/$prefix-output" 2>&1 &
else
# There will not be enough disk space, so do not collect.
warn "Collect canceled because there will not be enough disk space after collecting another $margin MB"
fi
# ##################################################################
@@ -909,7 +1059,9 @@ stalk() {
fi
# Purge old collect file between checks.
purge_samples "$OPT_DEST" "$OPT_RETENTION_TIME"
if [ -d "$OPT_DEST" ]; then
purge_samples "$OPT_DEST" "$OPT_RETENTION_TIME"
fi
done
}
@@ -924,23 +1076,8 @@ main() {
# Bash 4 has $BASHPID but we can't rely on that. Consequently,
# we don't know our own PID. See the usage of $! below.
RAN_WITH="--function=$OPT_FUNCTION --variable=$OPT_VARIABLE --threshold=$OPT_THRESHOLD --match=$OPT_MATCH --cycles=$OPT_CYCLES --interval=$OPT_INTERVAL --iterations=$OPT_ITERATIONS --run-time=$OPT_RUN_TIME --sleep=$OPT_SLEEP --dest=$OPT_DEST --prefix=$OPT_PREFIX --notify-by-email=$OPT_NOTIFY_BY_EMAIL --log=$OPT_LOG --pid=$OPT_PID"
log "Starting $0 $RAN_WITH"
# Make sure the collection dir exists.
if [ ! -d "$OPT_DEST" ]; then
mkdir -p "$OPT_DEST" || die "Cannot make --dest $OPT_DEST"
fi
# Check access to the --dest dir. By setting -x in the subshell,
# if either command fails, the subshell will exit immediately and
# $? will be non-zero.
(
set -e
touch "$OPT_DEST/test"
rm "$OPT_DEST/test"
)
if [ $? -ne 0 ]; then
die "Cannot read and write files to --dest $OPT_DEST"
fi
log "Starting $0 $RAN_WITH"
# Test if we have root; warn if not, but it isn't critical.
if [ "$(id -u)" != "0" ]; then
@@ -986,7 +1123,8 @@ if [ "$(basename "$0")" = "pt-stalk" ] \
po_status=$?
rm_tmpdir
if [ $po_status -ne 0 ]; then
exit $po_status
[ $OPT_ERRS -gt 0 ] && exit 1
exit 0
fi
# Now that we have the cmd line opts, check that we can actually
@@ -994,15 +1132,28 @@ if [ "$(basename "$0")" = "pt-stalk" ] \
[ -n "$(mysql $EXT_ARGV -e 'SELECT 1')" ] \
|| die "Cannot connect to MySQL. Check that MySQL is running and that the options after -- are correct."
if [ "$OPT_DAEMONIZE" = "yes" ]; then
# Check access to the --log file.
# Check existence and access to the --dest dir if we're collecting.
if [ "$OPT_COLLECT" ]; then
if [ ! -d "$OPT_DEST" ]; then
mkdir -p "$OPT_DEST" || die "Cannot make --dest $OPT_DEST"
fi
# Check access to the --dest dir. By setting -x in the subshell,
# if either command fails, the subshell will exit immediately and
# $? will be non-zero.
(
set -e
touch "$OPT_LOG"
touch "$OPT_DEST/test"
rm "$OPT_DEST/test"
)
if [ $? -ne 0 ]; then
die "Cannot write to --log $OPT_LOG"
die "Cannot read and write files to --dest $OPT_DEST"
fi
fi
if [ "$OPT_DAEMONIZE" ]; then
# Check access to the --log file.
touch "$OPT_LOG" || die "Cannot write to --log $OPT_LOG"
# The PID file will at first have our (parent) PID.
# This is fine for ensuring that only one of us is
@@ -1173,6 +1324,13 @@ interfaces for the port on which MySQL is listening. You can later use
pt-query-digest to decode the MySQL protocol and extract a log of query traffic
from it.
=item --config
type: string
Read this comma-separated list of config files. If specified, this must be the
first option on the command line.
=item --cycles
type: int; default: 5
@@ -1188,34 +1346,39 @@ its output as specified in --log.
=item --dest
type: string; default: ${HOME}/collected
type: string; default: /var/lib/pt-stalk
Where to store the diagnostic data. Each time the tool collects data, it writes
to a new set of files, which are named with the current system timestamp.
=item --disk-byte-limit
=item --disk-bytes-free
type: int; default: 100
type: size; default: 100M
Don't collect data unless the destination disk has this much free space. This
prevents the tool from filling up the disk with diagnostic data.
Don't collect data if the disk has less than this much free space.
This prevents the tool from filling up the disk with diagnostic data.
If the destination directory contains a previously captured sample of data, the
tool will measure its size and use that as an estimate of how much data is
If the L<"--dest"> directory contains a previously captured sample of data,
the tool will measure its size and use that as an estimate of how much data is
likely to be gathered this time, too. It will then be even more pessimistic,
and will refuse to collect data unless the disk has enough free space to hold
the sample and still have the desired amount of free space. For example, if
you'd like 100MB of free space and the previous diagnostic sample consumed
100MB, the tool won't collect any data unless the disk has 200MB free.
=item --disk-pct-limit
Valid size value suffixes are k, M, G, and T.
=item --disk-pct-free
type: int; default: 5
Don't collect data unless the disk has at least this percent free space. This
option works similarly to --disk-byte-limit, but specifies a percentage margin
of safety instead of a byte margin of safety. The tool honors both options, and
will not collect any data unless both margins are satisfied.
Don't collect data if the disk has less than this percent free space.
This prevents the tool from filling up the disk with diagnostic data.
This option works similarly to L<"--disk-bytes-free"> but specifies a
percentage margin of safety instead of a bytes margin of safety.
The tool honors both options, and will not collect any data unless both
margins are satisfied.
=item --function
@@ -1271,8 +1434,6 @@ MySQL options mentioned in the L<"SYNOPSIS"> above.
The plugin should not alter the tool's existing global variables. Prefix any
plugin-specific global variables with "PLUGIN_" or make them local.
=back
=item --help
Print help and exit.

View File

@@ -182,8 +182,8 @@ The syntax of the configuration files is as follows:
=item *
Whitespace followed by a hash (#) sign signifies that the rest of the line is a
comment. This is deleted.
Whitespace followed by a hash sign (#) signifies that the rest of the line is a
comment. This is deleted. For example:
=item *
@@ -200,7 +200,9 @@ Each line is permitted to be in either of the following formats:
option
option=value
Whitespace around the equals sign is deleted during processing.
Do not prefix the option with C<-->. Do not quote the values, even if
it has spaces; value are literal. Whitespace around the equals sign is
deleted during processing.
=item *
@@ -214,6 +216,22 @@ program.
=back
=head2 EXAMPLE
This config file for pt-stalk,
# Config for pt-stalk
variable=Threads_connected
cycles=2 # trigger if problem seen twice in a row
--
--user daniel
is equivalent to this command line:
pt-stalk --variable Threads_connected --cycles 2 -- --user daniel
Options after C<--> are passed literally to mysql and mysqladmin.
=head2 READ ORDER
The tools read several configuration files in order:

View File

@@ -61,7 +61,7 @@ collect() {
# Getting a GDB stacktrace can be an intensive operation,
# so do this only if necessary (and possible).
if [ "$CMD_GDB" -a "$OPT_COLLECT_GDB" = "yes" -a "$mysqld_pid" ]; then
if [ "$CMD_GDB" -a "$OPT_COLLECT_GDB" -a "$mysqld_pid" ]; then
$CMD_GDB \
-ex "set pagination 0" \
-ex "thread apply all bt" \
@@ -112,7 +112,7 @@ collect() {
# If TCP dumping is specified, start that on the server's port.
local tcpdump_pid=""
if [ "$CMD_TCPDUMP" -a "$OPT_COLLECT_TCPDUMP" = "yes" ]; then
if [ "$CMD_TCPDUMP" -a "$OPT_COLLECT_TCPDUMP" ]; 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} &
@@ -123,12 +123,12 @@ collect() {
# 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 [ "$CMD_OPCONTROL" -a "$OPT_COLLECT_OPROFILE" = "yes" ]; then
if [ "$CMD_OPCONTROL" -a "$OPT_COLLECT_OPROFILE" ]; then
if $CMD_OPCONTROL --init; then
$CMD_OPCONTROL --start --no-vmlinux
have_oprofile="yes"
fi
elif [ "$CMD_STRACE" -a "$OPT_COLLECT_STRACE" = "yes" ]; then
elif [ "$CMD_STRACE" -a "$OPT_COLLECT_STRACE" ]; 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=$!
@@ -178,8 +178,8 @@ collect() {
disk_space $d > $d/$p-disk-space
check_disk_space \
$d/$p-disk-space \
"$OPT_DISK_BYTE_LIMIT" \
"$OPT_DISK_PCT_LIMIT" \
"$OPT_DISK_BYTES_FREE" \
"$OPT_DISK_PCT_FREE" \
|| break
# Synchronize ourselves onto the clock tick, so the sleeps are 1-second
@@ -244,7 +244,7 @@ collect() {
"/path/to/mysqld'" \
> "$d/$p-opreport"
fi
elif [ "$CMD_STRACE" -a "$OPT_COLLECT_STRACE" = "yes" ]; then
elif [ "$CMD_STRACE" -a "$OPT_COLLECT_STRACE" ]; then
kill -s 2 $strace_pid
sleep 1
kill -s 15 $strace_pid

View File

@@ -1,4 +1,4 @@
# This program is copyright 2011 Percona Inc.
# This program is copyright 2011-2012 Percona Inc.
# Feedback and improvements are welcome.
#
# THIS PROGRAM IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
@@ -22,19 +22,38 @@
# parse_options parses Perl POD options from Bash tools and creates
# global variables for each option.
# ***********************************************************
# XXX
# GLOBAL $TMPDIR AND $TOOL MUST BE SET BEFORE USING THIS LIB!
# ***********************************************************
# XXX
# Parsing command line options with Bash is easy until we have to dealt
# with values that have spaces, e.g. --option="hello world". This is
# further complicated by command line vs. config file. From the command
# line, <--option "hello world"> is put into $@ as "--option", "hello world",
# i.e. 2 args. From a config file, <option=hello world> is either 2 args
# split on the space, or 1 arg as a whole line. It needs to be 2 args
# split on the = but this isn't possible; see the note before while read
# in _parse_config_files(). Perl tool config files do not work when the
# value is quoted, so we can't quote it either. And in any case, that
# wouldn't work because then the value would include the literal quotes
# because it's a line from a file, not a command line where Bash will
# interpret the quotes and return a single value in the code. So...
# XXX
# BE CAREFUL MAKING CHANGES TO THIS LIB AND MAKE SURE
# t/lib/bash/parse_options.sh STILL PASSES!
# XXX
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
ARGV="" # Non-option args (probably input files)
EXT_ARGV="" # Everything after -- (args for an external command)
HAVE_EXT_ARGV="" # Got --, everything else is put into EXT_ARGV
OPT_ERRS=0 # How many command line option errors
OPT_VERSION="" # If --version was specified
OPT_HELP="" # If --help was specified
PO_DIR="$TMPDIR/po" # Directory with program option spec files
# Sub: usage
@@ -61,21 +80,51 @@ usage() {
usage_or_errors() {
local file="$1"
if [ "$OPT_VERSION" = "yes" ]; then
if [ "$OPT_VERSION" ]; then
local version=$(grep '^pt-[^ ]\+ [0-9]' "$file")
echo "$version"
return 1
fi
if [ "$OPT_HELP" = "yes" ]; then
if [ "$OPT_HELP" ]; then
usage "$file"
echo
echo "Command line options:"
echo
for opt in $(ls $TMPDIR/po/); do
local desc=$(cat $TMPDIR/po/$opt | grep '^desc:' | sed -e 's/^desc://')
echo "--$opt"
echo " $desc"
perl -e '
use strict;
use warnings FATAL => qw(all);
my $lcol = 20; # Allow this much space for option names.
my $rcol = 80 - $lcol; # The terminal is assumed to be 80 chars wide.
my $name;
while ( <> ) {
my $line = $_;
chomp $line;
if ( $line =~ s/^long:/ --/ ) {
$name = $line;
}
elsif ( $line =~ s/^desc:// ) {
$line =~ s/ +$//mg;
my @lines = grep { $_ }
$line =~ m/(.{0,$rcol})(?:\s+|\Z)/g;
if ( length($name) >= $lcol ) {
print $name, "\n", (q{ } x $lcol);
}
else {
printf "%-${lcol}s", $name;
}
print join("\n" . (q{ } x $lcol), @lines);
print "\n";
}
}
' "$PO_DIR"/*
echo
echo "Options and values after processing arguments:"
echo
for opt in $(ls "$PO_DIR"); do
local varname="OPT_$(echo "$opt" | tr a-z- A-Z_)"
local varvalue="${!varname}"
printf -- " --%-30s %s" "$opt" "${varvalue:-(No value)}"
echo
done
return 1
@@ -108,10 +157,18 @@ parse_options() {
local file="$1"
shift
# Change --op=val to --op val because _parse_command_line() needs
# a space-separated list of "op val op val" etc.
local opts=$(echo "$@" | perl -ne 's/--(\S+)=/--$1 /g, print')
# XXX
# Reset all globals else t/lib/bash/parse_options.sh will fail.
# XXX
ARGV=""
EXT_ARGV=""
HAVE_EXT_ARGV=""
OPT_ERRS=0
OPT_VERSION=""
OPT_HELP=""
PO_DIR="$TMPDIR/po"
# Ready the directory for the program option (po) spec files.
if [ ! -d "$PO_DIR" ]; then
mkdir "$PO_DIR"
if [ $? -ne 0 ]; then
@@ -126,10 +183,25 @@ parse_options() {
exit 1
fi
_parse_pod "$file"
_eval_po
_parse_config_files
_parse_command_line $opts # do NOT quote, we want "--op" "val" not "--op val"
_parse_pod "$file" # Parse POD into program option (po) spec files
_eval_po # Eval po into existence with default values
# If the first option is --config FILES, then remove it and use
# those files instead of the default config files.
if [ $# -ge 2 ] && [ "$1" = "--config" ]; then
shift # --config
local user_config_files="$1"
shift # that ^
local IFS=","
for user_config_file in $user_config_files; do
_parse_config_files "$user_config_file"
done
else
_parse_config_files "/etc/percona-toolkit/percona-toolkit.conf" "/etc/percona-toolkit/$TOOL.conf" "$HOME/.percona-toolkit.conf" "$HOME/.$TOOL.conf"
fi
# Finally, parse the command line.
_parse_command_line "$@"
}
_parse_pod() {
@@ -178,13 +250,13 @@ _eval_po() {
# 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 "$PO_DIR"); do
local IFS=":"
for opt_spec in "$PO_DIR"/*; 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)
local size=0
while read key val; do
case "$key" in
long)
opt=$(echo $val | sed 's/-/_/g' | tr [:lower:] [:upper:])
@@ -195,6 +267,7 @@ _eval_po() {
"short form")
;;
type)
[ "$val" = "size" ] && size=1
;;
desc)
;;
@@ -204,13 +277,13 @@ _eval_po() {
fi
;;
*)
echo "Invalid attribute in $PO_DIR/$opt_spec: $line" >&2
echo "Invalid attribute in $opt_spec: $line" >&2
exit 1
esac
done < "$PO_DIR/$opt_spec"
done < "$opt_spec"
if [ -z "$opt" ]; then
echo "No long attribute in option spec $PO_DIR/$opt_spec" >&2
echo "No long attribute in option spec $opt_spec" >&2
exit 1
fi
@@ -221,25 +294,54 @@ _eval_po() {
fi
fi
# Convert sizes.
if [ $size -eq 1 -a -n "$default_val" ]; then
default_val=$(size_to_bytes $default_val)
fi
# Eval the option into existence as a global variable.
eval "OPT_${opt}"="$default_val"
done
}
_parse_config_files() {
local config_files="/etc/percona-toolkit/percona-toolkit.conf /etc/percona-toolkit/$TOOL.conf $HOME/.percona-toolkit.conf $HOME/.$TOOL.conf"
for config_file in $config_files; do
for config_file in "$@"; do
# Next config file if this one doesn't exist.
test -f "$config_file" || continue
# The config file syntax is just like a command line except there
# is one option per line. In Bash, --foo --bar is the same as
# --foo
# --bar
# So we can simply cat the config file into/as the command line.
# The Perl changes --foo=bar to --foo bar because _parse_command_line()
# needs a space-separated list of "opt val opt val" etc.
_parse_command_line \
$(cat "$config_file" | perl -ne 's/--(\S+)=/--$1 /g, print')
# We must use while read because values can contain spaces.
# Else, if we for $(grep ...) then a line like "op=hello world"
# will return 2 values: "op=hello" and "world". If we quote
# the command like for "$(grep ...)" then the entire config
# file is returned as 1 value like "opt=hello world\nop2=42".
while read config_opt; do
# Skip the line if it begins with a # or is blank.
echo "$config_opt" | grep '^[ ]*[^#]' >/dev/null 2>&1 || continue
# Strip leading and trailing spaces, and spaces around the first =,
# and end-of-line # comments.
config_opt="$(echo "$config_opt" | sed -e 's/^[ ]*//' -e 's/[ ]*\$//' -e 's/[ ]*=[ ]*/=/' -e 's/[ ]*#.*$//')"
# Skip blank lines.
[ "$config_opt" = "" ] && continue
# Options in a config file are not prefixed with --,
# but command line options are, so one or the other has
# to add or remove the -- prefix. We add it for config
# files rather than trying to strip it from command line
# options because it's a simpler operation here.
if ! [ "$HAVE_EXT_ARGV" ]; then
config_opt="--$config_opt"
fi
_parse_command_line "$config_opt"
done < "$config_file"
HAVE_EXT_ARGV="" # reset for each file
done
}
@@ -254,82 +356,138 @@ _parse_command_line() {
# 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.
local opt=""
local val=""
local next_opt_is_val=""
local opt_is_ok=""
local opt_is_negated=""
local real_opt=""
local required_arg=""
local spec=""
for opt in "$@"; do
if [ $# -eq 0 ]; then
break # no more opts
if [ "$opt" = "--" -o "$opt" = "----" ]; then
HAVE_EXT_ARGV=1
continue
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"
if [ "$HAVE_EXT_ARGV" ]; then
# Previous line was -- so this and subsequent options are
# really external argvs.
if [ "$EXT_ARGV" ]; then
EXT_ARGV="$EXT_ARGV $opt"
else
ARGV="$ARGV $opt"
EXT_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.
local required_arg=$(cat "$spec" | awk -F: '/^type:/{print $2}')
if [ -n "$required_arg" ]; then
if [ $# -eq 0 ]; then
if [ "$next_opt_is_val" ]; then
next_opt_is_val=""
if [ $# -eq 0 ] || [ $(expr "$opt" : "-") -eq 1 ]; then
OPT_ERRS=$(($OPT_ERRS + 1))
echo "$real_opt requires a $required_arg argument" >&2
continue
else
val="$1"
shift
fi
val="$opt"
opt_is_ok=1
else
if [ $neg -eq 0 ]; then
val="yes"
# If option does not begin with a hyphen (-), it's a filename, etc.
if [ $(expr "$opt" : "-") -eq 0 ]; then
if [ -z "$ARGV" ]; then
ARGV="$opt"
else
ARGV="$ARGV $opt"
fi
continue
fi
# Save real opt from cmd line for error messages.
real_opt="$opt"
# Strip leading -- or --no- from option.
if $(echo $opt | grep '^--no-' >/dev/null); then
opt_is_negated=1
opt=$(echo $opt | sed 's/^--no-//')
else
val="no"
opt_is_negated=""
opt=$(echo $opt | sed 's/^-*//')
fi
# Split opt=val pair.
if $(echo $opt | grep '^[a-z-][a-z-]*=' >/dev/null 2>&1); then
val="$(echo $opt | awk -F= '{print $2}')"
opt="$(echo $opt | awk -F= '{print $1}')"
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" | awk -F: '/^type:/{print $2}')
if [ "$required_arg" ]; then
# Option takes a value.
if [ "$val" ]; then
opt_is_ok=1
else
next_opt_is_val=1
fi
else
# Option does not take a value.
if [ "$val" ]; then
OPT_ERRS=$(($OPT_ERRS + 1))
echo "Option $real_opt does not take a value" >&2
continue
fi
if [ "$opt_is_negated" ]; then
val=""
else
val="yes"
fi
opt_is_ok=1
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:])
if [ "$opt_is_ok" ]; then
# 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"
# Convert sizes.
if grep "^type:size" "$spec" >/dev/null; then
val=$(size_to_bytes $val)
fi
# Re-eval the option to update its global variable value.
eval "OPT_$opt"="'$val'"
opt=""
val=""
next_opt_is_val=""
opt_is_ok=""
opt_is_negated=""
real_opt=""
required_arg=""
spec=""
fi
done
}
size_to_bytes() {
local size="$1"
echo $size | perl -ne '%f=(B=>1, K=>1_024, M=>1_048_576, G=>1_073_741_824, T=>1_099_511_627_776); m/^(\d+)([kMGT])?/i; print $1 * $f{uc($2 || "B")};'
}
# ###########################################################################
# End parse_options package
# ###########################################################################

View File

@@ -1,4 +1,4 @@
# This program is copyright 2011 Percona Inc.
# This program is copyright 2011-2012 Percona Inc.
# Feedback and improvements are welcome.
#
# THIS PROGRAM IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
@@ -33,46 +33,53 @@ disk_space() {
# Sub: check_disk_space
# Check if there is or will be enough disk space. Input is a file
# with output from <disk_space()>, i.e. `df -P -k`. The df output
# must use 1k blocks, but the mb arg from the user is in MB.
# must use 1k blocks, which should be POSIX standard.
#
# Arguments:
# file - File with output from <disk_space()>.
# mb - Minimum MB free.
# pc - Minimum percent free.
# mb_margin - Add this many MB to the real MB used.
# file - File with output from <disk_space()>.
# min_free_bytes - Minimum free bytes.
# min_free_pct - Minimum free percentage.
# bytes_margin - Add this many bytes to the real bytes 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}"
local min_free_bytes="${2:-0}"
local min_free_pct="${3:-0}"
local bytes_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))
# Real/actual bytes used and bytes free.
local used_bytes=$(cat "$file" | awk '/^\//{print $3 * 1024}');
local free_bytes=$(cat "$file" | awk '/^\//{print $4 * 1024}');
local pct_used=$(cat "$file" | awk '/^\//{print $5}' | sed -e 's/%//g');
local pct_free=$((100 - $pct_used))
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');
# Report the real values to the user.
local real_free_bytes=$free_bytes
local real_pct_free=$pct_free
if [ "$kb_margin" -gt "0" ]; then
local kb_total=$(($kb_used + $kb_free))
# If there's a margin, we need to adjust the real values.
if [ $bytes_margin -gt 0 ]; then
used_bytes=$(($used_bytes + $bytes_margin))
free_bytes=$(($free_bytes - $bytes_margin))
pct_used=$(awk "BEGIN { printf(\"%d\", ($used_bytes/($used_bytes + $free_bytes)) * 100) }")
kb_used=$(($kb_used + $kb_margin))
kb_free=$(($kb_free - $kb_margin))
pc_used=$(awk "BEGIN { printf(\"%d\", $kb_used/$kb_total * 100) }")
pct_free=$((100 - $pct_used))
fi
local pc_free=$((100 - $pc_used))
if [ $free_bytes -lt $min_free_bytes -o $pct_free -lt $min_free_pct ]; then
warn "Not enough free disk space:
Limit: ${min_free_pct}% free, ${min_free_bytes} bytes free
Actual: ${real_pct_free}% free, ${real_free_bytes} bytes free (- $bytes_margin bytes margin)
"
# Print the df that we used.
cat "$file" >&2
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
return 1 # not enough disk space
fi
return 0
return 0 # disk space is OK
}
# ###########################################################################

View File

@@ -5,6 +5,7 @@ TESTS=19
TMPFILE="$TEST_TMPDIR/parse-opts-output"
TMPDIR="$TEST_TMPDIR"
PATH="$PATH:$PERCONA_TOOLKIT_SANDBOX/bin"
TOOL="pt-stalk"
mkdir "$TMPDIR/collect" 2>/dev/null
@@ -14,7 +15,7 @@ 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
parse_options "$BIN_DIR/pt-stalk" --run-time 1 -- --defaults-file=/tmp/12345/my.sandbox.cnf
# Prefix (with path) for the collect files.
local p="$TMPDIR/collect/2011_12_05"
@@ -111,7 +112,7 @@ 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
parse_options "$BIN_DIR/pt-stalk" --run-time 2 -- --defaults-file=/tmp/12345/my.sandbox.cnf
rm $TMPDIR/collect/*

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash
TESTS=37
TESTS=73
TMPFILE="$TEST_TMPDIR/parse-opts-output"
TOOL="pt-stalk"
@@ -13,7 +13,7 @@ source "$LIB_DIR/parse_options.sh"
# Parse options from POD using all default values.
# ############################################################################
parse_options "$T_LIB_DIR/samples/bash/po001.sh" "" 2>$TMPFILE
parse_options "$T_LIB_DIR/samples/bash/po001.sh" 2>$TMPFILE
is "`cat $TMPFILE`" "" "No warnings or errors"
@@ -38,6 +38,8 @@ 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)"
is "$ARGV" "" "ARGV"
is "$EXT_ARGV" "" "External ARGV"
# ############################################################################
# --option=value should work like --option value.
@@ -47,6 +49,10 @@ parse_options "$T_LIB_DIR/samples/bash/po001.sh" --int-opt=42
is "$OPT_INT_OPT" "42" "Specified int option (--option=value)"
parse_options "$T_LIB_DIR/samples/bash/po001.sh" --string-opt="hello world"
is "$OPT_STRING_OPT" "hello world" "Specified int option (--option=\"value\")"
# ############################################################################
# Negate an option like --no-option.
# ############################################################################
@@ -56,7 +62,7 @@ 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 (neg)"
is "$OPT_TYPELESS_OPTION" "" "Default typeless option (neg)"
is "$OPT_NOPTION" "no" "Negated option (neg)"
is "$OPT_NOPTION" "" "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)"
@@ -68,6 +74,16 @@ is "$OPT_VERSION" "" "--version (neg)"
parse_options "$T_LIB_DIR/samples/bash/po001.sh" -v
is "$OPT_VERSION" "yes" "Short form"
# ############################################################################
# Command line options plus externals args.
# ############################################################################
parse_options "$T_LIB_DIR/samples/bash/po001.sh" --no-noption -- --foo
is "$OPT_NOPTION" "" "Negated option (--)"
is "$ARGV" "" "ARGV (--)"
is "$EXT_ARGV" "--foo" "External ARGV (--)"
# ############################################################################
# An unknown option should produce an error.
# ############################################################################
@@ -89,6 +105,14 @@ cmd_ok \
"grep -q \"For more information, 'man pt-stalk' or 'perldoc\" $TMPFILE" \
"--help"
cmd_ok \
"grep -q ' --string-opt2[ ]*String option with a default.' $TMPFILE" \
"Command line options"
cmd_ok \
"grep -q '\-\-string-opt[ ]*(No value)' $TMPFILE" \
"Options and values after processing arguments"
# Don't interpolate.
parse_options "$T_LIB_DIR/samples/bash/po003.sh" --help
usage_or_errors "$T_LIB_DIR/samples/bash/po003.sh" >$TMPFILE 2>&1
@@ -103,7 +127,7 @@ cmd_ok \
TOOL="pt-test"
cp "$T_LIB_DIR/samples/bash/config001.conf" "$HOME/.$TOOL.conf"
parse_options "$T_LIB_DIR/samples/bash/po001.sh" ""
parse_options "$T_LIB_DIR/samples/bash/po001.sh"
is "$OPT_STRING_OPT" "abc" "Default string option (conf)"
is "$OPT_STRING_OPT2" "foo" "Default string option with default (conf)"
@@ -112,16 +136,85 @@ is "$OPT_NOPTION" "yes" "Default neg option (conf)"
is "$OPT_INT_OPT" "" "Default int option (conf)"
is "$OPT_INT_OPT2" "42" "Default int option with default (conf)"
is "$OPT_VERSION" "" "--version (conf)"
is "$EXT_ARGV" "--host 127.1 --user daniel" "External ARGV (conf)"
is "$ARGV" "" "ARGV (conf)"
is "$EXT_ARGV" "--host=127.1 --user=daniel" "External ARGV (conf)"
# Command line should override config file.
parse_options "$T_LIB_DIR/samples/bash/po001.sh" --string-opt zzz
is "$OPT_STRING_OPT" "zzz" "Command line overrides config file"
# User-specified --config
parse_options "$T_LIB_DIR/samples/bash/po001.sh" --config "$T_LIB_DIR/samples/bash/config003.conf" --string-opt bar
is "$OPT_STRING_OPT" "bar" "--config string option"
is "$OPT_STRING_OPT2" "foo" "--config string option2"
is "$OPT_TYPELESS_OPTION" "" "--config typeless option"
is "$OPT_NOPTION" "yes" "--config negatable option"
is "$OPT_INT_OPT" "123" "--config int option"
is "$OPT_INT_OPT2" "42" "--config int option2"
is "$OPT_VERSION" "" "--config version option"
is "$ARGV" "" "--config ARGV"
is "$EXT_ARGV" "" "--config External ARGV"
# Multiple --config files, last should take precedence.
parse_options "$T_LIB_DIR/samples/bash/po001.sh" --config $T_LIB_DIR/samples/bash/config001.conf,$T_LIB_DIR/samples/bash/config002.conf
is "$OPT_STRING_OPT" "hello world" "Two --config string option"
is "$OPT_TYPELESS_OPTION" "yes" "Two --config typeless option"
is "$OPT_INT_OPT" "100" "Two --config int option"
is "$ARGV" "" "Two --config ARGV"
is "$EXT_ARGV" "--host=127.1 --user=daniel" "Two--config External ARGV"
# ############################################################################
# Option values with spaces.
# ############################################################################
# Config file
cp "$T_LIB_DIR/samples/bash/config002.conf" "$HOME/.$TOOL.conf"
parse_options "$T_LIB_DIR/samples/bash/po001.sh" ""
is "$OPT_STRING_OPT" "hello world" "Option value with space (conf)"
is "$OPT_INT_OPT" "100" "Option = value # comment (conf)"
rm "$HOME/.$TOOL.conf"
TOOL="pt-stalk"
# Command line
parse_options "$T_LIB_DIR/samples/bash/po001.sh" --string-opt "hello world"
is "$OPT_STRING_OPT" "hello world" "Option value with space (cmd line)"
is "$ARGV" "" "ARGV (cmd line)"
is "$EXT_ARGV" "" "External ARGV (cmd line)"
# ############################################################################
# Size options.
# ############################################################################
parse_options "$T_LIB_DIR/samples/bash/po004.sh" --disk-bytes-free 1T
is "$OPT_DISK_BYTES_FREE" "1099511627776" "Size: 1T"
parse_options "$T_LIB_DIR/samples/bash/po004.sh" --disk-bytes-free 1G
is "$OPT_DISK_BYTES_FREE" "1073741824" "Size: 1G"
parse_options "$T_LIB_DIR/samples/bash/po004.sh" --disk-bytes-free 1M
is "$OPT_DISK_BYTES_FREE" "1048576" "Size: 1M"
parse_options "$T_LIB_DIR/samples/bash/po004.sh" --disk-bytes-free 1K
is "$OPT_DISK_BYTES_FREE" "1024" "Size: 1K"
parse_options "$T_LIB_DIR/samples/bash/po004.sh" --disk-bytes-free 1k
is "$OPT_DISK_BYTES_FREE" "1024" "Size: 1k"
parse_options "$T_LIB_DIR/samples/bash/po004.sh" --disk-bytes-free 1
is "$OPT_DISK_BYTES_FREE" "1" "Size: 1"
parse_options "$T_LIB_DIR/samples/bash/po004.sh" --disk-bytes-free 100M
is "$OPT_DISK_BYTES_FREE" "104857600" "Size: 100M"
parse_options "$T_LIB_DIR/samples/bash/po004.sh"
is "$OPT_DISK_BYTES_FREE" "104857600" "Size: 100M default"
# ############################################################################
# Done
# ############################################################################

View File

@@ -18,36 +18,51 @@ is \
"2" \
"2-line df output"
check_disk_space "$SAMPLE/diskspace001.txt" 22000 18 >$TMPDIR/out 2>&1
# Filesystem 1024-blocks Used Available Capacity Mounted on
# /dev/disk0s2 118153176 94409664 23487512 81% /
#
# Those values are in Kb, so:
# used = 94409664 (94.4G) = 96_675_495_936 bytes
# free = 23487512 (23.4G) = 24_051_212_288 bytes
# pct free = 100 - 81 = 19 %
# want free - 100, 18 < 19, so this should be ok.
check_disk_space "$SAMPLE/diskspace001.txt" 24051212188 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
# want free - 100 is ok, but 20 < 19 is not.
check_disk_space "$SAMPLE/diskspace001.txt" 24051212188 20 >$TMPDIR/out 2>&1
is "$?" "1" "Not enough % free"
# want free + 100, so this should fail
# (real free is 100 bytes under what we want)
check_disk_space "$SAMPLE/diskspace001.txt" 24051212388 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" \
"grep -q 'Actual: 19% free, 24051212288 bytes free (- 0 bytes margin)' $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
# want free - 100 + 50 margin, so effectively want free - 50 is ok.
check_disk_space "$SAMPLE/diskspace001.txt" 24051212188 18 50
is "$?" "0" "Enough disk space with margin"
check_disk_space "$SAMPLE/diskspace001.txt" 23000 18 100 >$TMPDIR/out 2>&1
# want free - 100 + 101 margin, so real free is 1 byte under what we want.
check_disk_space "$SAMPLE/diskspace001.txt" 24051212188 18 101 >$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
# want free - 100 + 50 margin ok but %free will be 19 which is < 25.
check_disk_space "$SAMPLE/diskspace001.txt" 24051212188 25 50 >$TMPDIR/out 2>&1
is "$?" "1" "Not enough % free with margin"
cmd_ok \
"grep -q '3% free,' $TMPDIR/out" \
"grep -q 'Actual:[ ]*19% free,' $TMPDIR/out" \
"Calculates % free with margin"
# ###########################################################################

View File

@@ -1,5 +1,5 @@
--string-opt=abc
--typeless-option
string-opt=abc
typeless-option
--
--host=127.1
--user=daniel

View File

@@ -0,0 +1,5 @@
# Line comment.
string-opt=hello world
int-opt = 100 # Inline comment.

View File

@@ -0,0 +1,2 @@
string-opt=from config file
int-opt=123

View File

@@ -1,212 +0,0 @@
#!/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<pt-collect> tool.
=item --function
type: string; default: status
Built-in function name or plugin file name which returns the value of C<VARIABLE>.
Possible values are:
=over
=item * status
Grep the value of C<VARIABLE> from C<mysqladmin extended-status>.
=item * processlist
Count the number of processes in C<mysqladmin processlist> whose
C<VARIABLE> column matches C<MATCH>. 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<MATCH> 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<trg_plugin>. 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<THRESHOLD>. 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<pt-collect> 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<processles> L<"--function">.
=item --notify-by-email
type: string
Send mail to this list of addresses when C<pt-collect> 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<N> 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

View File

@@ -0,0 +1,37 @@
#!/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 --disk-bytes-free
type: size; default: 100M
Fall apart if there's less than this many bytes free on the disk.
=item --help
Print help.
=back
=head1 ENVIRONMENT
No env vars used.
=cut
DOCUMENTATION

View File

@@ -10,6 +10,7 @@ use strict;
use warnings FATAL => 'all';
use English qw(-no_match_vars);
use Test::More;
use Time::HiRes qw(sleep);
use PerconaTest;
use DSNParser;
@@ -23,7 +24,7 @@ if ( !$dbh ) {
plan skip_all => 'Cannot connect to sandbox master';
}
else {
plan tests => 15;
plan tests => 20;
}
my $cnf = "/tmp/12345/my.sandbox.cnf";
@@ -168,6 +169,67 @@ like(
"Trigger file logs how pt-stalk was ran"
);
# ###########################################################################
# Triggered but --no-collect.
# ###########################################################################
diag(`rm $pid_file 2>/dev/null`);
diag(`rm $log_file 2>/dev/null`);
diag(`rm $dest/* 2>/dev/null`);
(undef, $uptime) = $dbh->selectrow_array("SHOW STATUS LIKE 'Uptime'");
$threshold = $uptime + 2;
$retval = system("$trunk/bin/pt-stalk --no-collect --iterations 1 --dest $dest --variable Uptime --threshold $threshold --cycles 1 --run-time 1 --pid $pid_file -- --defaults-file=$cnf >$log_file 2>&1");
sleep 2;
$output = `cat $log_file`;
like(
$output,
qr/Collect triggered/,
"Collect triggered"
);
ok(
! -f "$dest/*",
"No files collected"
);
$output = `ps x | grep -v grep | grep 'pt-stalk pt-stalk --iterations 1 --dest $dest'`;
is(
$output,
"",
"pt-stalk is not running"
);
# #############################################################################
# --config
# #############################################################################
diag(`cp $ENV{HOME}/.pt-stalk.conf $ENV{HOME}/.pt-stalk.conf.original 2>/dev/null`);
diag(`cp $trunk/t/pt-stalk/samples/config001.conf $ENV{HOME}/.pt-stalk.conf`);
system "$trunk/bin/pt-stalk --dest $dest --pid $pid_file >$log_file 2>&1 &";
PerconaTest::wait_for_files($pid_file);
sleep 1;
chomp($pid = `cat $pid_file`);
$retval = system("kill $pid 2>/dev/null");
is(
$retval >> 0,
0,
"Killed pt-stalk"
);
$output = `cat $log_file`;
like(
$output,
qr/Check results: Aborted_connects=|variable=Aborted_connects/,
"Read default config file"
);
diag(`rm $ENV{HOME}/.pt-stalk.conf`);
diag(`cp $ENV{HOME}/.pt-stalk.conf.original $ENV{HOME}/.pt-stalk.conf 2>/dev/null`);
# #############################################################################
# Done.
# #############################################################################

View File

@@ -0,0 +1,8 @@
--iterations=1
--variable=Aborted_connects
--threshold=999999
--
-umsandbox
-pmsandbox
--host 127.1
--port 12345