Friday, April 1, 2016

Apache Tuner Script






#######################################
          APACHE TUNER SCRIPT
#####################################







#!/usr/bin/perl
#
# author: srinivas kanneboyina
# version: 0.3
# description: This will make some recommendations on tuning your Apache 
# configuration based on your current settings and Apache's memory usage
#
# acknowledgements: This script was inspired by Major Hayden's MySQL Tuner
# (http://mysqltuner.com). 

use diagnostics;
use Getopt::Long qw(:config no_ignore_case bundling pass_through);
use POSIX;
use strict;
use Term::ANSIColor;

# here we're going to build a list of the files included by the Apache 
# configuration
sub build_list_of_files {
 my ($base_apache_config,$apache_root) = @_;

 # these to arrays will contain lists of Apache configuration files
 # this is going to be the ultimate list of files that will be parsed 
 # searching for arguments
 my @master_list;
 # this will be a "scratch" space to store a list of files that 
 # currently need to be searched for more "include" lines
 my @find_includes_in;

 # put the main configuration file into the list of files we're going
 # to include
 push(@master_list,$base_apache_config);

 # put the main configuratino file into the list of files we need to 
 # search for include lines
 push(@find_includes_in,$base_apache_config);

 #get the Include lines from the main apache config
 @master_list = find_included_files(\@master_list,\@find_includes_in,$apache_root);
}

# here we're going to build an array holding the content of all of the 
# available configuration files
sub build_config_array {
 my ($base_apache_config,$apache_root) = @_;

 # these to arrays will contain lists of Apache configuration files
 # this is going to be the ultimate list of files that will be parsed 
 # searching for arguments
 my @master_list;

 # this will be a "scratch" space to store a list of files that 
 # currently need to be searched for more "include" lines
 my @find_includes_in;

 # put the main configuration file into the list of files we're going
 # to include
 push(@master_list,$base_apache_config);

 # put the main configuratino file into the list of files we need to 
 # search for include lines
 push(@find_includes_in,$base_apache_config);

 #get the Include lines from the main apache config
 @master_list = find_included_files(\@master_list,\@find_includes_in,$apache_root);
}

# this will find all of the files that need to be included
sub find_included_files {
 my ($master_list, $find_includes_in, $apache_root) = @_; 

 # get the number of elements in the array
 my $count = @$find_includes_in;

 # this array will eventually hold the entire apache configuration
 my @master_config_array;

 # while there are still entries in this array, keep processing
 while ( $count > 0 ) {
  my $file = $$find_includes_in[0];
  
  print "VERBOSE: Processing ".$file."\n" if $main::VERBOSE;

  if(-d $file && $file !~ /\*$/) {
   print "VERBOSE: Adding glob to ".$file.", is a directory\n" if $main::VERBOSE;
   $file .= "/" if($file !~ /\/$/);
   $file .= "*";
  }

  # open the file
  open(FILE,$file) || die("Unable to open file: ".$file."\n");
  
  # push the file into an array
  my @file = <FILE>;

  # put the file in the master configuration array
  push(@master_config_array,@file);

  # close the file
  close(FILE);

  # search the file for includes
  foreach (@file) {

   # this will be used to store a list of any new include
   # lines found
   # my @new_includes; 

   # if the line looks like an include, then we want to examine it
   if ( $_ =~ m/^\s*include/i ) {
    # grab the included file name or file glob
    $_ =~ s/\s*include\s+(.+)\s*/$1/i;

    # strip out any quoting
    $_ =~ s/['"]+//g;

    # prepend the Apache root for files or
    # globs that are relative
    if ( $_ !~ m/^\// ) {
     $_ = $apache_root."/".$_;
    }

    # check for file globbing

    if(-d $_ && $_ !~ /\*$/) {
     print "VERBOSE: Adding glob to ".$_.", is a directory\n" if $main::VERBOSE;
     $_ .= "/" if($_ !~ /\/$/);
     $_ .= "*";
    }

    if ( $_ =~ m/.*\*.*/ ) {
     my $glob = $_;
     my @include_files;
     chomp($glob);

     # if the include is a file glob,
     # expand it and add the files
     # to the list
     my @new_includes = expand_included_files(\@include_files, $glob, $apache_root);
     push(@$master_list,@new_includes);
     push(@$find_includes_in,@new_includes);
    }
    else {
     # if it is not a glob, push the 
     # line into the configuration 
     # array
     push(@$master_list,$_);
     push(@$find_includes_in,$_);
    }
   }
  }
  # trim the first entry off the array now that we have 
  # processed it
  shift(@$find_includes_in);

  # get the new count of files left to look at
  $count = @$find_includes_in;
 }

 # return the config array with the included files attached
 return @master_config_array;
}

# this will expand a glob into a list of individual files
sub expand_included_files {
 my ($include_files, $glob, $apache_root) = @_;

 # use a call to ls to get a list of the files from the glob
 my @files = `ls $glob 2> /dev/null`;

 # add the files from the glob to the array we're going to pass back
 foreach(@files) {
  chomp($_);
  push(@$include_files,$_);
  print "VERBOSE: Adding ".$_." to list of files for processing\n" if $main::VERBOSE;
 }

 # return the include_files array with the files from the glob attached
 return @$include_files;
}

# search the configuration array for a defined value that is not inside of a 
# virtual host
sub find_master_value {
 my ($config_array, $model, $config_element) = @_;

 # store our results in an array
 my @results;

 # used to control whether or not we are currently ignoring elements 
 # while searching the array
 my $ignore = 0;

 my $ignore_by_model = 0;
 my $ifmodule_count = 0;

 # apache has two available models - prefork and worker. only one can be
 # in use at a time. we have already determined which model is being 
 # used
 my $ignore_model;
 
 if ( $model =~ m/.*worker.*/i ) {
  $ignore_model = "prefork";
 } else {
  $ignore_model = "worker";
 }

 print "VERBOSE: Searching Apache configuration for the ".$config_element." directive\n" if $main::VERBOSE;

 # search for the string in the configuration array
 foreach (@$config_array) {
  # ignore lines that are comments
  if ( $_ !~ m/^\s*#/ ) {
   chomp($_);

   # we ignore lines that are within a Directory, Location, 
   # File, or Virtualhost block
  
   # check to see if we have an opening tag for one of the 
   # block types listed above
   if ( $_ =~ m/^\s*<(directory|location|files|virtualhost|ifmodule\s.*$ignore_model)/i ) {
    #print "Starting to ignore lines: ".$_."\n";
    $ignore = 1;
   }
   # check for a closing block to stop ignoring lines
   if ( $_ =~ m/^\s*<\/(directory|location|files|virtualhost|ifmodule)/i ) {
    #print "Starting to watch lines: ".$_."\n";
    $ignore = 0;
   }

   # if we're not ignoring lines, check and see if we've 
   # found the configuration element we're looking for
   if ( $ignore != 1 ) {  
    # if we find a match
    if ( $_ =~ m/^\s*$config_element\s+.*/i ) {
     chomp($_);
     $_ =~ s/^\s*$config_element\s+(.*)/$1/i;
     push(@results,$_);
    }
   }
  }
 }

 # if we find multiple definitions for the same element, we should 
 # return the last one
 my $result;

 if ( @results > 1 ) {
  $result = $results[@results - 1];
 }
 else {
  $result = $results[0];
 }

 #Result not found
 if (@results == 0) {
  $result = "CONFIG NOT FOUND";
 }

 print "VERBOSE: $result " if $main::VERBOSE;
 # Ubuntu does not store the Apache user, group, or pidfile definitions 
 # in the apache2.conf file. instead, variables are in the configuration 
 # file and the real values are in /etc/apache2/envvars. this is a 
 # workaround for that behavior.
 if ( $config_element =~ m/[users|group|pidfile]/i && $result =~ m/^\$/i ) {
  if ( -e "/etc/debian_version" && -e "/etc/apache2/envvars") {
   print "VERBOSE: Using Ubuntu workaround for: ".$config_element."\n" if $main::VERBOSE;
   print "VERBOSE: Processing /etc/apache2/envvars\n" if $main::VERBOSE;

   open(ENVVARS,"/etc/apache2/envvars") || die "Could not open file: /etc/apache2/envvars\n"; 
   my @envvars = <ENVVARS>;
   close(ENVVARS);

   # change "pidfile" to match Ubuntu's "pid_file" 
   # definition
   if ( $config_element =~ m/pidfile/i ) {
    $config_element = "pid_file";
   }

   foreach (@envvars) {
    if ( $_ =~ m/.*$config_element.*/i ) {
     chomp($_);
     $_ =~ s/^.*=(.*)\s*$/$1/i;
     $result = $_;
    }
   }
  }
 }

 # return the value to the main program
 return $result;
}

# this will examine the memory usage of the apache processes and return one of
# three different outputs: average usage across all processes, the memory usage
# by the largest process, or the memory usage by the smallest process
sub get_memory_usage {
 my ($process_name, $apache_user, $search_type) = @_;

 my (@proc_mem_usages, $result);

 # get a list of the pid's for apache running as the appropriate user
 my @pids = `ps aux | grep $process_name | grep -v root | grep $apache_user | awk \'{ print \$2 }\'`;

 # figure out how much memory each process is using
 foreach (@pids) {
  chomp($_);

  # pmap -d is used to determine the memory usage for the 
  # individual processes
  my $pid_mem_usage = `pmap -d $_ | grep writeable/private | awk \'{ print \$4 }\'`;
  $pid_mem_usage =~ s/K//;
  chomp($pid_mem_usage);

  print "VERBOSE: Memory usage by PID ".$_." is ".$pid_mem_usage."K\n" if $main::VERBOSE;
  
  # on a busy system, the grep output will return the pid for the
  # grep process itself, which will be gone by the time we get 
  # around to running pmap
  if ( $pid_mem_usage ne "" ) {
   push(@proc_mem_usages, $pid_mem_usage);
  }
 }

 # examine the array 
 if ( $search_type eq "high" ) {
  # to find the largest process, sort the values from largest to
  # smallest and take the first one
  @proc_mem_usages = sort { $b <=> $a } @proc_mem_usages;
  $result = $proc_mem_usages[0] / 1024;
 }
 if ( $search_type eq "low" ) {
  # to find the smallest process, sort the values from smallest to
  # largest and take the first one
  @proc_mem_usages = sort { $a <=> $b } @proc_mem_usages;
  $result = $proc_mem_usages[0] / 1024;
 }
 if ( $search_type eq "average" ) {
  # to get the average, add up the total amount of memory used by
  # each process, and then divide by the number of processes
  my $sum = 0; 
  my $count;
  foreach (@proc_mem_usages) {
   $sum = $sum + $_;
   $count++;
  } 

  # our result is in kilobytes, convert it to megabytes before 
  # returning it
  $result = $sum / $count / 1024;
 }
 
 # round off the result
 $result = round($result);

 return $result;
}

# this function accepts the path to a file and then tests to see whether the 
# item at that path is an Apache binary
sub test_process {
 my ($process_name) = @_;

        # Reduce to only aphanumerics, to deal with "nginx: master process" or any newlnes
        $process_name = `echo -n $process_name | sed 's/://g'`;

 # the first line of output from "httpd -V" should tell us whether or
 # not this is Apache
 my @output = `$process_name -V 2>&1`;

 print "VERBOSE: First line of output from \"$process_name -V\": $output[0]" if $main::VERBOSE;

 my $return_val = 0;
        #if ( $output eq '' ) {
        #    $return_val = 0;
        #}

 # check for output matching Apache'
        if ( $output[0] =~ m/^Server version.*Apache\/[0-9].*/ ) {
  $return_val = 1;
 } 

 return $return_val;
}

# this will return the pid for the process listening on the port specified
sub get_pid {
 my ( $port ) = @_;

 # find the pid for the software listening on the specified port. this
 # might return multiple values depending on Apache's listen directives
 my @pids = `netstat -ntap | grep LISTEN | grep \":$port \" | awk \'{ print \$7 }\' | cut -d / -f 1`;

 print "VERBOSE: ".@pids." found listening on port 80\n" if $main::VERBOSE;

 # set an initial, invalid PID. 
 my $pid = 0;;
 foreach (@pids) {
  chomp($_);
  $_ =~ s/(.*)\/.*/$1/;
  if ( $pid == 0 ) {
   $pid = $_;
  }
  elsif ( $pid != $_ ) {
   print "There are multiple PIDs listening on port $port.";
   exit;
  }
  else { 
   $pid = $_;
  }
 }

        # Fallback - Look for root owned httpd|apache2 process
        #if ( $pid eq '' ) {
        #   $pid = `ps aux | grep -v grep | egrep \'^root.*(httpd|apache2)\$\' | awk \'{print \$2}\'`;
        #}

 # return the pid, or 0 if there is no process found
 if ( $pid eq '' ) {
  $pid = 0;
 }

 print "VERBOSE: Returning a PID of ".$pid."\n" if $main::VERBOSE;

 return $pid;
}

# this will return the path to the application running with the specified pid
sub get_process_name {
 my ( $pid ) = @_;

 print "VERBOSE: Finding process running with a PID of ".$pid."\n" if $main::VERBOSE;

 # based on the process name, we can figure out where the binary lives
 my $process_name = `ps ax | grep "\^[[:space:]]*$pid\[[:space:]]" | awk \'{print \$5 }\'`;
 chomp($process_name);

 print "VERBOSE: Found process ".$process_name."\n" if $main::VERBOSE;

 # return the process name, or 0 if there is no name found
 if ( $process_name eq '' ) {
  $process_name = 0;
 }

 return $process_name;
}

# this will return the apache root directory when given the full path to an
# Apache binary
sub get_apache_root {
 my ( $process_name ) = @_;
 # use the identified Apache binary to figure out where the root directory is 
 # for the Apache instance
 my $apache_root = `$process_name -V | grep \"HTTPD_ROOT\"`;
 $apache_root =~ s/.*=\"(.*)\"/$1/;
 chomp($apache_root);

 if ( $apache_root eq '' ) {
  $apache_root = 0;
 }

 return $apache_root;
}

# this will return the apache configuration file, relative to the apache root
# for the provided apache binary
sub get_apache_conf_file {
 my ( $process_name ) = @_;
 my $apache_conf_file = `$process_name -V | grep \"SERVER_CONFIG_FILE\"`;
 $apache_conf_file =~ s/.*=\"(.*)\"/$1/;
 chomp($apache_conf_file);

 # return the apache configuration file, or 0 if there is no result
 if ( $apache_conf_file eq '' ) {
  $apache_conf_file = 0;
 }

 return $apache_conf_file;
}

# this will determine whether this apache is using the worker or the prefork
# model based on the way the binary was built
sub get_apache_model {
 my ( $process_name ) = @_;
 my $model = `$process_name -l | egrep "worker.c|prefork.c"`;
 chomp($model);
 $model =~ s/\s*(.*)\.c/$1/;

 # return the name of the MPM, or 0 if there is no result
 if ( $model eq '' ) {
  $model = 0 ;
 }

 return $model;
}

# this will get the Apache version string
sub get_apache_version {
 my ( $process_name ) = @_;
 my $version = `$process_name -V | grep "Server version"`;
 chomp($version);
 $version =~ s/.*:\s(.*)$/$1/;

 if ( $version eq '' ) {
  $version = 0;
 }

 return $version
}

# this will us ps to determine the Apache uptime. it returns an array 
sub get_apache_uptime {
 my ( $pid ) = @_;

 # this will return the running time for the given pid in the format 
 # "days-hours:minutes:seconds"
 my $uptime = `ps -eo \"\%p \%t\" | grep $pid | grep -v grep | awk \'{ print \$2 }\'`;
 chomp($uptime);

 print "VERBOSE: Raw uptime: $uptime\n" if $main::VERBOSE;

 # check to see if we've been running for multiple days
 my ($days, $hours, $minutes, $seconds);
 if ( $uptime =~ m/^.*-.*:.*:.*$/ ) { 
  $days = $uptime;
  $days =~ s/([0-9]*)-.*/$1/;

  # trim the days off of our uptime value
  $uptime =~ s/.*-(.*)/$1/;
 
  ($hours, $minutes, $seconds) = split(':', $uptime);
 }
 elsif ( $uptime =~ m/^.*:.*:.*/ ) {
  $days = 0;
  ($hours, $minutes, $seconds) = split(':', $uptime);
 }
 elsif ( $uptime =~ m/^.*:.*/) {
  $days = 0;
  $hours = 0;
  ($minutes, $seconds) = split(':', $uptime); 
 }
 else {
  $days = 0;
  $hours = 0;
  $minutes = 00;
  $seconds = 00;
 }

 # push everything into an array to pass back
 my @apache_uptime = ( $days, $hours, $minutes, $seconds );

 

 return @apache_uptime;
}

# return the global value for a PHP setting
sub get_php_setting {
 my ( $php_bin, $element ) = @_; 

 # this will return an array with all of the local and global PHP 
 # settings
 my @php_config_array = `php -r "phpinfo(4);"`;

 my @results;

 # search the array for our desired setting
 foreach (@php_config_array) {
  chomp($_);
  if ( $_ =~ m/^\s*$element\s*/ ) {
   chomp($_);
   $_ =~ s/.*=>\s+(.*)\s+=>.*/$1/;
   push(@results, $_);
  }
 }

        # if we find multiple definitions for the same element, we should 
        # return the last one (just in case)
        my $result;

        if ( @results > 1 ) {
                $result = $results[@results - 1];
        }
        else {
                $result = $results[0];
        }

 # some PHP directives are measured in MB. we want to trim the "M" off
 # here for those that are
 $result =~ s/^(.*)M$/$1/;

        # return the value to the main program
        return $result;
}

sub generate_standard_report {
 my ( $available_mem, $maxclients, $apache_proc_highest, $model, $threadsperchild ) = @_;


 # print a report header
 print color 'bold white' if ! $main::NOCOLOR;
 print "### GENERAL REPORT ###\n";
 print color 'reset' if ! $main::NOCOLOR;

 # show what we're going to use to generate our numbers
 print "\nSettings considered for this report:\n\n";

 print "\tYour server's physical RAM:\t\t";
 print color 'bold' if ! $main::NOCOLOR;
 print $available_mem."MB\n";
 print color 'reset' if ! $main::NOCOLOR;

 print "\tApache's MaxClients directive:\t\t";
 print color 'bold' if ! $main::NOCOLOR;
 print $maxclients."\n";
 print color 'reset' if ! $main::NOCOLOR;

 print "\tApache MPM Model:\t\t\t";
 print color 'bold' if ! $main::NOCOLOR;
 print $model ."\n";
 print color 'reset' if ! $main::NOCOLOR;

 print "\tLargest Apache process (by memory):\t";
 print color 'bold' if ! $main::NOCOLOR;
 print $apache_proc_highest."MB\n";
 print color 'reset' if ! $main::NOCOLOR;

 if ($model eq "prefork") {
  # based on the Apache memory usage (size of the largest process, 
  # check to see if the maxclients setting for Apache is sane
  my $max_rec_maxclients = $available_mem / $apache_proc_highest;
  $max_rec_maxclients = floor($max_rec_maxclients);

  # determine the maximum potential memory usage by Apache
  my $max_potential_usage = $maxclients * $apache_proc_highest;
  my $max_potential_usage_pct = round(($max_potential_usage/$available_mem)*100);
  if ( $maxclients <= $max_rec_maxclients ) {
   print color 'bold green' if ! $main::NOCOLOR;
   print "[ OK ]"; 
   print color 'reset' if ! $main::NOCOLOR;
   print "\tYour MaxClients setting is within an acceptable range.\n";
   print "\tMax potential memory usage: \t\t";
   print color 'bold white' if ! $main::NOCOLOR;
   print $max_potential_usage." MB" ;
   print color 'reset';
   print "\n\n";

   print "\tPercentage of RAM allocated to Apache\t";
   print color 'bold white' if ! $main::NOCOLOR;
   print $max_potential_usage_pct." %" ;
   print color 'reset';
   print "\n\n";
  }
  else {
   print color 'bold red' if ! $main::NOCOLOR;
   print "[ !! ]";
   print color 'reset' if ! $main::NOCOLOR;
   print "\tYour MaxClients setting is too high. It should be no greater than ";
   print color 'bold white' if ! $main::NOCOLOR;
   print $max_rec_maxclients.".\n";
   print color 'reset';
   print "\tMax potential memory usage: ";
   print color 'bold white' if ! $main::NOCOLOR;
   print $max_potential_usage." MB" ."($max_potential_usage_pct % of available RAM)" ;
   print color 'reset';
   print "\n\n";

   print "\tPercentage of RAM allocated to Apache\t\t";
   print color 'bold white' if ! $main::NOCOLOR;
   print $max_potential_usage_pct." %" ;
   print color 'reset';
   print "\n\n";
  }
 }
 if ($model eq "worker") {
  my $max_rec_maxclients = int((($available_mem/$apache_proc_highest) * $threadsperchild)/25)*25;

  my $max_potential_usage = ($maxclients/$threadsperchild) * $apache_proc_highest;
  $max_potential_usage = round($max_potential_usage);
  my $max_potential_usage_pct = round(($max_potential_usage/$available_mem)*100);
  if ( $maxclients <= $max_rec_maxclients ) {
   print color 'bold green' if ! $main::NOCOLOR;
   print "[ OK ]"; 
   print color 'reset' if ! $main::NOCOLOR;
   print "\tYour MaxClients setting is within an acceptable range.\n";
   print "\t(Max potential memory usage: ";
   print color 'bold white' if ! $main::NOCOLOR;
   print $max_potential_usage." MB" ."($max_potential_usage_pct % of available RAM)" ;
   print color 'reset';
   print ")\n\n";
   
   print "\tPercentage of RAM allocated to Apache\t\t";
   print color 'bold white' if ! $main::NOCOLOR;
   print $max_potential_usage_pct." %" ;
   print color 'reset';
   print "\n\n";

  }
  else {
   print color 'bold red' if ! $main::NOCOLOR;
   print "[ !! ]";
   print color 'reset' if ! $main::NOCOLOR;
   print "\tYour MaxClients setting is too high. It should be no greater than ";
   print color 'bold white' if ! $main::NOCOLOR;
   print $max_rec_maxclients.".\n";
   print color 'reset';
   print "\tMax potential memory usage: ";
   print color 'bold white' if ! $main::NOCOLOR;
   print $max_potential_usage." MB" ."($max_potential_usage_pct % of available RAM)" ;
   print color 'reset';
   print ")\n\n";

   print "\tPercentage of RAM allocated to Apache\t\t";
   print color 'bold white' if ! $main::NOCOLOR;
   print $max_potential_usage_pct." %" ;
   print color 'reset';
   print "\n\n";
  }
 }

 print "-----------------------------------------------------------------------\n";
}

# generate the optional report based on the server's PHP settings
sub generate_php_report {
 my ( $available_mem, $maxclients ) = @_;

 # get the php memory_limit setting
 my $apache_proc_php = get_php_setting('/usr/bin/php', 'memory_limit');

 # make a second recommendation based on potential PHP memory usage
 my $max_rec_maxclients = $available_mem / $apache_proc_php;
 $max_rec_maxclients = floor($max_rec_maxclients);

 # calculate the largest potential memory usage
 my $max_potential_usage = $apache_proc_php * $maxclients;

 # print a report header
 print color 'bold white' if ! $main::NOCOLOR;
 print "### PHP REPORT ###\n";
 print color 'reset' if ! $main::NOCOLOR;

 # show what we're going to use to generate our numbers
 print "\nSettings considered for this report:\n\n";
 print "\tYour server's physical RAM:\t\t";
 print color 'bold' if ! $main::NOCOLOR;
 print $available_mem."MB\n";
 print color 'reset' if ! $main::NOCOLOR;
 print "\tApache's MaxClients directive:\t\t";
 print color 'bold' if ! $main::NOCOLOR;
 print $maxclients."\n";
 print color 'reset' if ! $main::NOCOLOR;
 print "\tPHP's memory_limit setting:\t\t";
 print color 'bold' if ! $main::NOCOLOR;
 print $apache_proc_php."MB\n";
 print color 'reset' if ! $main::NOCOLOR;
 print "-----------------------------------------------------------------------\n";

 # see if the maxclients directive is below the calculated threshold
 if ( $maxclients <= $max_rec_maxclients ) {
  print color 'bold green' if ! $main::NOCOLOR;
  print "[ OK ]";
  print color 'reset' if ! $main::NOCOLOR;
  print "\tYour MaxClients setting is within an acceptable range.\n";
  print "\t(max potential memory usage by PHP under Apache: ";
  print color 'bold white';
  print $max_potential_usage."MB";
  print color 'reset';
  print ")\n\n";
 }
 else {
  print color 'bold red' if ! $main::NOCOLOR;
  print "[ !! ]";
  print color 'reset' if ! $main::NOCOLOR;
  print "\tYour MaxClients setting is too high. It should be no greater\n\tthan ";
  print color 'bold white' if ! $main::NOCOLOR;
  print $max_rec_maxclients.".\n";
  print color 'reset';
  print "\t(max potential memory usage by PHP under Apache: ";
  print color 'bold white';
  print $max_potential_usage."MB";
  print color 'reset';
  print ")\n\n";
 }
}

# this rounds a value to the nearest hundreth
sub round {
 my ( $value ) = @_;

 # add five thousandths
 $value = $value + 0.005;

 # truncat the result
 $value = sprintf("%.2f", $value);

 return $value;
} 

#Return the number of CPU cores
sub get_cores {
    my $cmd = 'egrep ' . "'". '^physical id|^core id|^$' . "'" . ' /proc/cpuinfo | awk '. "'" . 'ORS=NR%3?",":"\n"' . "'" . '| sort | uniq | wc -l';
    my $cmd_out = `$cmd`;
    chomp $cmd_out;
    return $cmd_out;
}


# print usage
sub usage {
 print "Usage: apachebuddy.pl [OPTIONS]\n";
 print "If no options are specified, the basic tests will be run.\n";
 print "\n";
 print "\t-h, --help\tPrint this help message\n";
 print "\t-p, --port=PORT\tSpecify an alternate port to check (default: 80)\n";
 print "\t-P, --php\tInclude the PHP memory_limit setting when making the recommendation\n";
 print "\t-v, --verbose\tUse verbose output (this is very noisy, only useful for debugging)\n";
 print "\n";
}

# print a header
sub print_header {
 print color 'bold white' if ! $main::NOCOLOR;
 print "########################################################################\n";
 print "# Apache Buddy v 0.3 ###################################################\n";
 print "########################################################################\n";
 print color 'reset' if ! $main::NOCOLOR;
}

########################
# GATHER CMD LINE ARGS #
########################

# if help is not asked for, we do not give it
my $help = "";

# if no port is specified, we default to 80
my $port = 80;

# by default, do not include PHP in the check
my $php = 0;

# by default, do not use verbose output
our $VERBOSE = "";

# by default, use color output
our $NOCOLOR = 0;

# grab the command line arguments
GetOptions('help|h' => \$help, 'port|p:i' => \$port, 'php|P' => \$php, 'verbose|v' => \$VERBOSE, 'nocolor' => \$main::NOCOLOR);

# check for invalid options, bail if we find any and print the usage output
if ( @ARGV > 0 ) {
 print "Invalid option: ";
 foreach (@ARGV) {
  print $_." ";
 }
 print "\n";
 usage;
 exit;
}

########################
# BEGIN MAIN EXECUTION #
########################

# make sure the script is being run as root
my $uid = `id -u`;
chomp($uid);

print "VERBOSE: UID of user is: ".$uid."\n" if $VERBOSE;

# we need to run as root to ensure that we can access all of the appropriate 
# files
if ( $uid ne '0' ) {
 print "This script must be run as root.\n";
 exit;
}

# this script uses pmap to determine the memory mapped to each apache 
# process. make sure that pmap is available.
my $pmap = `which pmap`;
chomp($pmap);

# make sure that pmap is available within our path
if ( $pmap !~ m/.*\/pmap/ ) { 
 print "Unable to locate the pmap utility. This script requires pmap to analyze Apache's memory consumption.\n";
 exit;
}

# make sure PHP is available before we proceed
if ( $php == 1 ) {
 # check to see if there is a binary called "php" in our path
 my $check = `which php`;

 if ( $check eq '' ) {
  print "Unable to locate the PHP binary.\n";

  my $path = `echo \$PATH`;
  chomp($path);
  print "VERBOSE: Path: $path\n" if $VERBOSE;

  exit;
 }
}

# if the user has added the help flag, or if they have defined a port  
if ( $help eq 1 || $port eq 0 ) {
 usage();
 exit;
}
elsif ( $port < 0 || $port > 65534 ) {
 print "INVALID PORT: $port\n";
 print "Valid port numbers are 1-65534\n";
 exit;
}
else {
 # print the header
 print_header;

 print color 'bold white' if ! $NOCOLOR;
 print "Gathering information...\n";
 print color 'reset' if ! $NOCOLOR;

 # first thing we do is get the pid of the process listening on the 
 # specified port
 print "We are checking the service running on port ".$port."\n";
 my $pid = get_pid($port);

 print "VERBOSE: PID is ".$pid."\n" if $VERBOSE;

 if ( $pid eq 0 ) {
  print "Unable to determine PID of the process.";
  exit;
 }

 # now we get the name of the process running with the specified pid
 my $process_name = get_process_name($pid);
 print "The process listening on port ".$port." is ".$process_name."\n";
 if ( $process_name eq 0 ) {
  print "Unable to determine the name of the process.";
  exit;
 }

 # check to see if there is a file in the file system at the path 
 # identified
 #if  ( ! -e $process_name ) {
 # print "File .".$process_name." does not exist.\n";
 # exit;
 #}

 # check to see if the process we have identified is Apache
 my $is_it_apache = test_process($process_name);

 if ( $is_it_apache == 1 ) {
  my $apache_version = get_apache_version($process_name);

  print "VERBOSE: Apache version: $apache_version\n" if $VERBOSE;

  # if we received a "0", just print "Apache"
  if ( $apache_version eq 0 ) {
   $apache_version = "Apache"; 
  }

  print "The process running on port $port is $apache_version\n";
 }
 else {
  print "The process running on port $port is not Apache. \n Falling back to process list...\n";
                $pid = `ps aux | grep -v grep | egrep \'^root\.\*(httpd|apache2)\$\' | awk \'BEGIN {ORS=\"\"} {print \$2}\' `;
                if ( $pid eq '' ) {
                    print "Could not find Apache process. Exiting...\n";
        exit;
                } else {
                    # If we found it, then reset the proces_name
                    $process_name = get_process_name($pid);
                } 
 }

 # determine the Apache uptime
 my @apache_uptime = get_apache_uptime($pid);
 print "Apache has been running ".$apache_uptime[0]."d ".$apache_uptime[1]."h ".$apache_uptime[2]."m ".$apache_uptime[3]."s\n";

 # find the apache root 
 my $apache_root = get_apache_root($process_name);

 print "VERBOSE: The Apache root is: ".$apache_root."\n" if $VERBOSE;
 
 # find the apache configuration file (relative to the apache root)
 my $apache_conf_file = get_apache_conf_file($process_name);
 print "VERBOSE: The Apache config file is: ".$apache_conf_file."\n" if $VERBOSE;

 # piece together the full path to the configuration file, if a server 
 # does not have the HTTPD_ROOT value defined in its apache build, then
 # try just using the path to the configuration file
 my $full_apache_conf_file_path;
 if ( -e $apache_conf_file ) {
  $full_apache_conf_file_path = $apache_conf_file;
  print "The full path to the Apache config file is: ".$full_apache_conf_file_path."\n";
 }
 elsif ( -e $apache_root."/".$apache_conf_file ) {
  $full_apache_conf_file_path = $apache_root."/".$apache_conf_file;
  print "The full path to the Apache config file is: ".$full_apache_conf_file_path."\n";
 }
 else {
  print "Apache configuration file does not exist: ".$full_apache_conf_file_path."\n";
  exit;
 }

 # find out if we're using worker or prefork
 my $model = get_apache_model($process_name); 
 if ( $model eq 0 ) {
  print "Unable to determine whether Apache is using worker or prefork\n";
 }
 else {
  print "Apache is using $model model\n";
 }

 print color 'bold white' if ! $NOCOLOR;
 print "\nExamining your Apache configuration...\n";
 print color 'reset' if ! $NOCOLOR;

 # get the entire config, including included files, into an array
 my @config_array = build_config_array($full_apache_conf_file_path,$apache_root);

 # determine what user apache runs as 
 my $apache_user = find_master_value(\@config_array, $model, 'user');
 if (length($apache_user) > 8) {
                $apache_user = `id -u $apache_user`;
                chomp($apache_user);
        }
 print "Apache runs as ".$apache_user."\n";

 # determine what the max clients setting is 
 my $maxclients = find_master_value(\@config_array, $model, 'maxclients');
 $maxclients = 256 if($maxclients eq 'CONFIG NOT FOUND');
 print "Your max clients setting is ".$maxclients."\n";

 #calculate ThreadsPerChild. This is useful for the worker MPM calculations
        my $threadsperchild = find_master_value(\@config_array, $model, 'threadsperchild');
        my $serverlimit = find_master_value(\@config_array, $model, 'serverlimit');

 if ($model eq "worker") {
  print "Your ThreadsPerChild setting for worker MPM is  ".$threadsperchild."\n";
  print "Your ServerLimit setting for worker MPM is  ".$serverlimit."\n";
 }

 print color 'bold white' if ! $NOCOLOR;
 print "\nAnalyzing memory use...\n";
 print color 'reset' if ! $NOCOLOR;

 # figure out how much RAM is in the server
 my $available_mem = `free | grep \"Mem:\" | awk \'{ print \$2 }\'` / 1024;
 $available_mem = floor($available_mem);

 print "Your server has ".$available_mem." MB of memory\n";

 my $apache_proc_highest = get_memory_usage($process_name, $apache_user, 'high');
 my $apache_proc_lowest = get_memory_usage($process_name, $apache_user, 'low');
 my $apache_proc_average = get_memory_usage($process_name, $apache_user, 'average');


 if ( $model eq "prefork") {
  print "The largest apache process is using ".$apache_proc_highest." MB of memory\n";
  print "The smallest apache process is using ".$apache_proc_lowest." MB of memory\n";
  print "The average apache process is using ".$apache_proc_average." MB of memory\n";

  my $average_potential_use = $maxclients * $apache_proc_average;
  $average_potential_use = round($average_potential_use);
  my $average_potential_use_pct = round(($average_potential_use/$available_mem)*100);
  print "Going by the average Apache process, Apache can potentially use ".$average_potential_use." MB RAM ($average_potential_use_pct % of available RAM)\n" ;

  my $highest_potential_use = $maxclients * $apache_proc_highest;
  $highest_potential_use = round($highest_potential_use);
  my $highest_potential_use_pct = round(($highest_potential_use/$available_mem)*100);
  print "Going by the largest Apache process, Apache can potentially use ".$highest_potential_use." MB RAM ($highest_potential_use_pct % of available RAM)\n" ;
 }

 if ( $model eq "worker") {
  print "The largest apache process is using ".$apache_proc_highest." MB of memory\n";
  print "The smallest apache process is using ".$apache_proc_lowest." MB of memory\n";
  print "The average apache process is using ".$apache_proc_average." MB of memory\n";

  my $highest_potential_use = ($maxclients/$threadsperchild) * $apache_proc_highest;
  $highest_potential_use = round($highest_potential_use);
  my $highest_potential_use_pct = round(($highest_potential_use/$available_mem)*100);
  print "Going by the largest Apache process, Apache can potentially use ".$highest_potential_use." MB RAM ($highest_potential_use_pct % of available RAM)\n" ;
 }
 print color 'bold white' if ! $NOCOLOR;
 print "\nGenerating reports...\n";
 print color 'reset' if ! $NOCOLOR;

 # determine which report we're generating
 if ( $php == 1 ) {
  generate_php_report($available_mem, $maxclients);
 }
 else {
  generate_standard_report($available_mem, $maxclients, $apache_proc_highest, $model, $threadsperchild);
 }
}

print "-----------------------------------------------------------------------\n";

No comments:

Post a Comment