#!/usr/bin/perl ################################################################################################################### # $Id:: check 20 2007-02-23 14:26:44Z fabrice $ # $Revision:: 20 $ ################################################################################################################### # Authors : Fabrice Dulaunoy # # Copyright (C) 2006-2007 Fabrice Dulaunoy # # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation; either version 2 of the License, or (at your # option) any later version. See . # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. ################################################################################################################### # ################################################################################################################### use strict; package MyPackage; use Config::General; use Getopt::Std; use LWP::UserAgent; use URI; use File::Basename; # CVS VSERSION #my $VERSION = do { my @rev = ( q$Revision: 20 $ =~ /\d+/g ); sprintf "%d." . "%d" x $#rev, @rev }; # SVN VERSION my $VERSION = sprintf "1.%02d", '$Revision: 20 $ ' =~ /(\d+)/; my %option; getopts( "vHhc:", \%option ); if ( $option{ h } ) { print "Usage: $0 [options ...]\n\n"; print "Where options include:\n"; print "\t -h \t\t\tthis help (what else ?)\n"; print "\t -H \t\t\tshow a sample config file\n"; print "\t -v \t\t\tprint version and exit\n"; print "\t -c file \t\tuse config file (default /etc/check.conf)\n"; print "\n\t This is a small program parsing the config file \n"; print "\t and checking one or more condition on one or more servers\n"; print "\t these condition could be \n"; print "\t\t HTTP return code list (with optinal Host Header and optional Basic Authentication) \n"; print "\t\t a regex over a HTTP GET (with optinal Host Header and optional Basic Authentication)\n"; print "\t\t a regex over a FTP GET ( with optional Basic Authentication)\n"; print "\t\t a TCP open port\n"; print "\t the result state is an AND over all tests \n"; print "\t this result could be \n"; print "\t\t a simple HTTP return state (\"200 OK\" or \"503 Service Unavailable\" \n"; print "\t\t a HTML page with a status OK or NOK for each test\n"; print "\t\t a HTML page with a staus OK or NOK for each test in a row of a TABLE\n"; print "\n\t The natural complement of this tools is the poll_check tool\n"; print "\t The result code of this tools is designed to fit the HAPROXY requirement (test over a port not related to the WEB server)\n"; } if ( $option{ H } ) { print "\t A sample config file could be:\n"; print <<'EOF'; ########################################################### # listening port ( default 9898 ) port 9899 # on which IP to bind (default 127.0.0.1 ) * = all IP host 10.2.1.1 # which client addr is allow ( default 127.0.0.0/8 ) #cidr_allow = 0.0.0.0/0 # verbosity from 0 to 4 (default 0 = no log ) log_level = 1 # daemonize (default 0 = no ) daemon = 1 # content put a HTML content after header # (default 0 = no content 1 = html 2 = table ) content = 2 # reparse the config file at each request ( default 0 = no ) # only SIGHUP reread the config file) reparse = 1 # pid_file (default /var/run/check.pid ) # $$$ = basename of config file # $$ = PID pid_file=/var/run/CHECK_$$$.pid # log_file (default /var/log/check.log ) # $$$ = basename of config file # $$ = PID log_file=/var/log/CHECK_$$$.log # number of servers to keep running (default = 5) min_servers = 2 # number of servers to have waiting for requests (default = 2) min_spare_servers = 1 # maximum number of servers to have waiting for requests (default = 10) max_spare_servers =1 # number of servers (default = 50) max_servers = 2 ########################################################### # a server to check # type could be get , regex or tcp # # get = do a http or ftp get and check the result code with # the list, coma separated, provided ( default = 200,201 ) # hostheader is optional and send to the server if provided # # regex = do a http or ftp get and check the content result # with regex provided # hostheader is optional and send to the server if provided # # tcp = test if the tcp port provided is open # ########################################################### url=http://127.0.0.1:80/apache2-default/index.html type = get code=200,201 hostheader = www.test.com url=http://127.0.0.1:82/apache2-default/index.html type = get code=200,201 hostheader = www.myhost.com url= http://10.2.2.1 type = regex regex= /qdAbm/ type = tcp url = 10.2.2.1 port =80 type = get url = ftp://USER:PASSWORD@10.2.3.1 code=200,201 ########################################################### EOF } if ( $option{ h } || $option{ H } ) { exit; } if ( $option{ v } ) { print "$VERSION\n"; exit; } use vars qw(@ISA); use Net::Server::PreFork; @ISA = qw(Net::Server::PreFork); my $port; my $host; my $reparse; my $cidr_allow; my $log_level; my $log_file; my $pid_file; my $daemon; my $min_servers; my $min_spare_servers; my $max_spare_servers; my $max_servers; my $html_content; my $conf_file = $option{ c } || "/etc/check.conf"; my $pwd = $ENV{ PWD }; $conf_file =~ s/^\./$pwd/; $conf_file =~ s/^([^\/])/$pwd\/$1/; my $basename = basename( $conf_file, ( '.conf' ) ); my $CONF = parse_conf( $conf_file ); my $reparse_one = 0; $SIG{ HUP } = sub { $reparse_one = 1; }; my @TEST; my $test_list = $CONF->{ realserver }; if ( ref( $test_list ) eq "ARRAY" ) { @TEST = @{ $test_list }; } else { @TEST = ( $test_list ); } my $server = MyPackage->new( { port => $port, host => $host, cidr_allow => $cidr_allow, log_level => $log_level, child_communication => 1, setsid => $daemon, log_file => $log_file, pid_file => $pid_file, min_servers => $min_servers, min_spare_servers => $min_spare_servers, max_spare_servers => $max_spare_servers, max_servers => $max_servers, } ); $server->run(); exit; sub process_request { my $self = shift; if ( $reparse || $reparse_one ) { $CONF = parse_conf( $conf_file ); } my $result; my @TEST; my $test_list = $CONF->{ realserver }; if ( ref( $test_list ) eq "ARRAY" ) { @TEST = @{ $test_list }; } else { @TEST = ( $test_list ); } my $allow_code; my $test_item; my $html_data; foreach my $test ( @TEST ) { my $uri; my $authority; my $URL = $test->{ url }; $uri = URI->new( $URL ); $authority = $uri->authority; if ( exists $test->{ type } ) { if ( $test->{ type } =~ /get/i ) { my $allow_code = $test->{ code } || '200,201'; $test_item++; my $host = $test->{ hostheader } || $authority; my $res = get( $URL, $allow_code, $host ); if ( $html_content == 1 ) { if ( $res ) { $html_data .= "GET OK $URL
\r\n"; } else { $html_data .= "GET NOK $URL
\r\n"; } } if ( $html_content == 2 ) { if ( $res ) { $html_data .= "GETOK$URL\r\n"; } else { $html_data .= "GETNOK$URL\r\n"; } } $result += $res; } if ( $test->{ type } =~ /regex/i ) { my $regex = $test->{ regex }; $test_item++; my $host = $test->{ hostheader } || $authority; my $res = regex( $URL, $regex, $host ); if ( $html_content == 1 ) { if ( $res ) { $html_data .= "REGEX OK $URL
\r\n"; } else { $html_data .= "REGEX NOK $URL
\r\n"; } } if ( $html_content == 2 ) { if ( $res ) { $html_data .= "REGEXOK$URL\r\n"; } else { $html_data .= "REGEXNOK$URL\r\n"; } } $result += $res; } if ( $test->{ type } =~ /tcp/i ) { $test_item++; my $PORT = $test->{ port } || 80; my $res = TCP( $URL, $PORT ); if ( $html_content == 1 ) { if ( $res ) { $html_data .= "TCP OK $URL
\r\n"; } else { $html_data .= "TCP NOK $URL
\r\n"; } } if ( $html_content == 2 ) { if ( $res ) { $html_data .= "TCPOK$URL\r\n"; } else { $html_data .= "TCPNOK$URL\r\n"; } } $result += $res; } } } my $len; if ( $html_content == 1 ) { $html_data = "\r\n\r\n$html_data\r\n"; $len = ( length( $html_data ) ) - 2; } if ( $html_content == 2 ) { $html_data = "\r\n\r\n$html_data
\r\n"; $len = ( length( $html_data ) ) - 2; } if ( $result != $test_item ) { my $header = "HTTP/1.0 503 Service Unavailable\r\n"; if ( $html_content ) { $header .= "Content-Length: $len\r\nContent-Type: text/html; charset=iso-8859-1\r\n"; } print $header . $html_data; return; } my $header = "HTTP/1.0 200 OK\r\n"; if ( $html_content ) { $header .= "Content-Length: $len\r\nContent-Type: text/html; charset=iso-8859-1\r\n"; } print $header. $html_data; } 1; ########################################################## ########################################################## # function to REGEX on a GET from URL # arg: uri # regex to test (with extra parameter like perl e.g. /\bweb\d{2,3}/i ) # IP # port (optionnal: default=80) # ret: 0 if no reply # 1 if reply ########################################################## ########################################################## sub regex { my $url = shift; my $regex = shift; my $host = shift; $regex =~ /\/(.*)\/(.*)/; my $reg = $1; my $ext = $2; my %options; $options{ 'agent' } = "LB_REGEX_PROBE/$VERSION"; $options{ 'timeout' } = 10; my $ua = LWP::UserAgent->new( %options ); my $response = $ua->get( $url, "Host" => $host ); if ( $response->is_success ) { my $html = $response->content; if ( $ext =~ /i/ ) { if ( $html =~ /$reg/si ) { return 1; } } else { if ( $html =~ /$reg/s ) { return 1; } } } return 0; } ########################################################## ########################################################## # function to GET an URL (HTTP or FTP) ftp://FTPTest:6ccount4F@brice!@172.29.0.146 # arg: uri # allowed code (comma seaparated) # IP # port (optionnal: default=80) # ret: 0 if not the expected vcode # 1 if the expected code is returned ########################################################## ########################################################## sub get { my $url = shift; my $code = shift; my $host = shift; $code =~ s/\s*//g; my %codes = map { $_ => $_ } split /,/, $code; my %options; $options{ 'agent' } = "LB_HTTP_PROBE/$VERSION"; $options{ 'timeout' } = 10; my $ua = LWP::UserAgent->new( %options ); my $response = $ua->get( $url, "Host" => $host ); if ( $response->is_success ) { my $rc = $response->{ _rc }; if ( defined $codes{ $rc } ) { return 1; } } return 0; } ########################################################## ########################################################## # function to test a port on a host # arg: hostip # port # timeout # ret: 0 if not open # 1 if open ########################################################## ########################################################## sub TCP { use IO::Socket::PortState qw(check_ports); my $remote_host = shift; my $remote_port = shift; my $timeout = shift; my %porthash = ( tcp => { $remote_port => { name => 'to_test', } } ); check_ports( $remote_host, $timeout, \%porthash ); return $porthash{ tcp }{ $remote_port }{ open }; } ############################################## # parse config file # IN: File PATH # Out: Ref to a hash with config data ############################################## sub parse_conf { my $file = shift; my $conf = new Config::General( -ConfigFile => $file, -ExtendedAccess => 1, -AllowMultiOptions => "yes" ); my %config = $conf->getall; $port = $config{ port } || 9898; $host = $config{ host } || '127.0.0.1'; $reparse = $config{ reparse } || 0; $cidr_allow = $config{ cidr_allow } || '127.0.0.0/8'; $log_level = $config{ log_level } || 0; $log_file = $config{ log_file } || "/var/log/check.log"; $pid_file = $config{ pid_file } || "/var/run/check.pid"; $daemon = $config{ daemon } || 0; $min_servers = $config{ min_servers } || 5; $min_spare_servers = $config{ min_spare_servers } || 2; $max_spare_servers = $config{ max_spare_servers } || 10; $max_servers = $config{ max_servers } || 50; $html_content = $config{ content } || 0; $pid_file =~ s/\$\$\$/$basename/g; $pid_file =~ s/\$\$/$$/g; $log_file =~ s/\$\$\$/$basename/g; $log_file =~ s/\$\$/$$/g; if ( !( keys %{ $config{ realserver } } ) ) { die "No farm to test\n"; } return ( \%config ); }