├── CHANGELOG ├── README.md ├── exabgp.conf.sample ├── healthcheck.conf.sample └── healthcheck.pl /CHANGELOG: -------------------------------------------------------------------------------- 1 | v0.4.4 - 13/05/16 2 | ------------------------------ 3 | BUG FIXES 4 | * Certain quoting for the check command will cause the wrong return code - 5 | strip the quotes from start and end. 6 | * Perl error when the script starts up under ExaBGP 7 | 8 | v0.4.3 - 13/05/16 9 | ------------------------------ 10 | BUG FIXES 11 | * Changes to interval, rise/fall values and logcheck values were not 12 | being reloaded correctly. 13 | * Certain changes that resulted in a configuration error that was detected 14 | when reloading the config resulted in no further changes being applied. 15 | * If the configuration file disappeared during a certain time (eg. it was 16 | removed) it would cause the script to crash. 17 | 18 | v0.4.2 - 13/05/16 19 | ------------------------------ 20 | BUG FIXES 21 | * Fix the process name - if the service was rising or falling it may not 22 | be updated in some cases. 23 | * Many configuration file reload enhancements: 24 | - All IP's are not withdrawn and announced again when they don't need to be 25 | - If there were multiple configuration file changes, not all of them were 26 | applied correctly - only some changes may have been applied. 27 | - If the next hop IP was changed and the service was in the down state, it 28 | was not being applied when the service came back up. 29 | - The whole configuration reload part of the script has been cleaned up so 30 | that it isn't handled differently depending on the current service status. 31 | 32 | v0.4.1 - 12/05/16 33 | ------------------------------ 34 | NEW FEATURES 35 | * Use the Data::Validate::IP module for validating IP addresses. The 36 | nexthop IP and any IP's to announce are validated to ensure that they 37 | are valid and belong to the same address family. If there is a mask 38 | specified they are also validated. 39 | * If no netmask is specified for IP's to be announced, default to /32 40 | or /128. 41 | 42 | v0.4 - 09/05/16 43 | ------------------------------ 44 | NEW FEATURES 45 | * Add version option to script - call ./healthcheck.pl -v to get version. 46 | * Change log file added. 47 | * Extra debug logging added - if debug=yes is set for the service and the 48 | list of IP's configured for the service is changed, the old and new IP's 49 | will be logged to the debug log file. 50 | 51 | BUG FIXES 52 | * If the configuration file is edited to change the IP's announced, when 53 | it is reloaded automatically the changed IP is not detected. This only 54 | affects IP changes that have the same number of IP's as before the 55 | change. The script now requires the Perl module Array::Utils. 56 | 57 | v0.3 - 29/04/16 58 | ------------------------------ 59 | BUG FIXES 60 | * Fix error when re-reading configuration file due to changes due to 61 | incorrect arguments passed to validate_config(). 62 | 63 | v0.2 - 22/04/16 64 | ------------------------------ 65 | BUG FIXES 66 | * Fix typos in help output/readme 67 | 68 | v0.1 - 21/04/16 69 | ------------------------------ 70 | * Initial release to GitHub 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ExaBGP-Healthcheck 2 | 3 | **NOT MAINTAINED** 4 | 5 | This project is no longer maintained. Various more modern alternatives are available, for example: 6 | 7 | - [ExaCheck](https://exacheck.net) 8 | - [ExaBGP healthcheck.py](https://github.com/Exa-Networks/exabgp/blob/main/src/exabgp/application/healthcheck.py) 9 | 10 | Although this utility may still work any bugs will not be fixed. 11 | 12 | ----- 13 | 14 | ExaBGP healthcheck is a simple Perl script for use with [ExaBGP] which can control the announcing of routes based on the status of health checks. The health checks can be anything you like - the script only cares about the exit code (0 is success, anything else is a failure). 15 | 16 | Features: 17 | * IPv4 and IPv6 support 18 | * Configuration file changes are applied automatically, no need to reload/restart services. The configuration file is verified before applying changes, if there are errors the changes will not be applied. 19 | * Health checks can call your own scripts, normal CLI utilities etc. 20 | * Multiple services can be defined with different IP addresses 21 | * Easy monitoring of service status - plain text files contain the current status of services, the image name for the process also has the current status 22 | * Command line options to view the current status, validate config etc. 23 | * Logging (with debug options) 24 | 25 | As an example of what this can be used for, see [my blog post]. 26 | 27 | All documentation for this project is available in the [Wiki], I recommend having a quick read through. 28 | 29 | ## System Requirements 30 | An installation of Perl is required with the following modules: 31 | ``` 32 | Array::Utils 33 | Config::IniFiles 34 | Data::Validate::IP 35 | Digest::MD5::File 36 | File::Basename 37 | File::Pid 38 | Getopt::Long 39 | Log::Log4perl 40 | Scalar::Util 41 | Switch 42 | Time::Piece 43 | ``` 44 | 45 | For more information, see the [System Requirements wiki page]. 46 | 47 | ## Installation 48 | Copy healthcheck.pl to the ExaBGP directory, usually `/etc/exabgp`. Create a configuration file for the health check script in `/etc/exabgp/healthcheck.conf`. 49 | 50 | Configure ExaBGP as normal and add the appropriate process for the Neighbor, eg.: 51 | ``` 52 | process myservice { 53 | run /etc/exabgp/healthcheck.pl -c announce -n myservice; 54 | } 55 | ``` 56 | 57 | For complete instructions, see the [Installation wiki page]. 58 | 59 | ## Configuration File 60 | A sample configuration file is included for both ExaBGP and healthcheck.pl. 61 | 62 | For complete configuration options, see the [Configuration File wiki page]. 63 | 64 | ## Command Line Usage 65 | For a list of available options, call the script with the '-help' switch: 66 | ``` 67 | /etc/exabgp/healthcheck.pl -help 68 | ``` 69 | 70 | For full instuctions, see the [CLI Usage wiki page]. 71 | 72 | [//]: # (Links to other sites/projects) 73 | 74 | [ExaBGP]: 75 | [my blog post]: 76 | [Wiki]: 77 | [System Requirements wiki page]: 78 | [Installation wiki page]: 79 | [quick start page]: 80 | [Configuration File wiki page]: 81 | [CLI Usage wiki page]: 82 | -------------------------------------------------------------------------------- /exabgp.conf.sample: -------------------------------------------------------------------------------- 1 | neighbor 192.168.1.254 { 2 | 3 | # Neighbor Settings 4 | description "Sample BGP neighbor"; 5 | router-id 192.168.1.1; 6 | local-address 192.168.1.1; 7 | local-as 64512; 8 | peer-as 64512; 9 | hold-time 10; 10 | graceful-restart 60; 11 | group-updates true; 12 | 13 | family { 14 | inet4 unicast unicast; 15 | inet6 unicast unicast; 16 | } 17 | 18 | process ipv4service { 19 | run /etc/exabgp/healthcheck.pl -c announce -n ipv4service; 20 | } 21 | process ipv6service { 22 | run /etc/exabgp/healthcheck.pl -c announce -n ipv6service; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /healthcheck.conf.sample: -------------------------------------------------------------------------------- 1 | # For complete configuration instructions see the wiki page: 2 | # https://github.com/shthead/exabgp-healthcheck/wiki/Configuration-File 3 | 4 | [global] 5 | 6 | # The interval determins how often health checks are ran (in seconds). 7 | # An interval of 3 will wait 3 seconds between each health check. 8 | interval=3 9 | 10 | # Wait N seconds for the check command to execute. The timeout must be 11 | # lower than the interval. If the timeout is reached the service is assumed 12 | # down and the process of withdrawing the route begins. 13 | timeout=2 14 | 15 | # The MED of all routes being announced. Must be between 1 and 1000. 16 | metric=100 17 | 18 | # How many times the service check must pass before the service is considered 19 | # up. If this is set to 5, the service must pass 5 health checks before the 20 | # route will be announced. 21 | rise=5 22 | 23 | # How many times the service check must fail before the service is considered 24 | # down. If this is set to 5, the service must fail 5 health checks before the 25 | # route will be withdrawn. 26 | fall=3 27 | 28 | # Log location. This must be a full path to a file. If the file does not exist 29 | # it will be created (if the permissions allow it). If the file cannot be 30 | # created the script will error and exit. Errors will be logged to 31 | # [filename].err and debug to [filename].debug (if enabled). 32 | logfile=/var/log/healthcheck/healthcheck 33 | 34 | # Log the output of the check command to the debug log. Requires debug=yes 35 | logcheck=no 36 | 37 | # Enable the debug log 38 | debug=no 39 | 40 | [ipv4service] 41 | 42 | # The next hop IP address for the route to be advertised with BGP. This will 43 | # usually be the servers main IP address. This IP must belong to the same 44 | # address family as the IP's being announced. 45 | nexthop=192.168.1.1 46 | 47 | # The command to use for health check. This can be anything you like - a bash 48 | # one liner or a script etc. The command that is executed MUST have an exit 49 | # code of 0 for success. Any other exit code is considered fail. 50 | command="/usr/local/scripts/check_ipv4.sh" 51 | 52 | # IP addresses to announce. You can specify as many as you like. Ensure that 53 | # the IP addresses all belong to the same address family as the nexthop IP. 54 | # If you do not specify a mask for the IP address it is assumed to be /128 for 55 | # IPv6 and /32 for IPv4. 56 | ip=10.1.1.1 57 | ip=10.1.1.2 58 | 59 | # If this file exists the service will be considered down. You can use this to 60 | # disable the service easily - simple touch the file and the routes will be 61 | # withdrawn. Once you are done, remove the file and the service will start 62 | # being checked again. 63 | disable=/etc/exabgp/healthcheck_ipv4service.disable 64 | 65 | 66 | [ipv6service] 67 | 68 | # The next hop IP address for the route to be advertised with BGP. This will 69 | # usually be the servers main IP address. This IP must belong to the same 70 | # address family as the IP's being announced. 71 | nexthop=fd12:3456:ffff::1 72 | 73 | # The command to use for health check. This can be anything you like - a bash 74 | # one liner or a script etc. The command that is executed MUST have an exit 75 | # code of 0 for success. Any other exit code is considered fail. 76 | command="/usr/local/scripts/check_ipv6.sh" 77 | 78 | # IP addresses to announce. You can specify as many as you like. Ensure that 79 | # the IP addresses all belong to the same address family as the nexthop IP. 80 | # If you do not specify a mask for the IP address it is assumed to be /128 for 81 | # IPv6 and /32 for IPv4. 82 | ip=fd12:3456:ffff:1::1 83 | ip=fd12:3456:ffff:1::2 84 | 85 | # If this file exists the service will be considered down. You can use this to 86 | # disable the service easily - simple touch the file and the routes will be 87 | # withdrawn. Once you are done, remove the file and the service will start 88 | # being checked again. 89 | disable=/etc/exabgp/healthcheck_ipv6service.disable 90 | -------------------------------------------------------------------------------- /healthcheck.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | ######################################################################################## 4 | # ExaBGP - Health Checking Script 5 | # GitHub project page: https://github.com/sysadminblog/exabgp-healthcheck 6 | ######################################################################################## 7 | # This script is used with ExaBGP to control BGP announcements to various services. 8 | # The IP's are only announced if the health check passes. 9 | # See the GitHub page for more information. 10 | 11 | use strict; 12 | use warnings; 13 | 14 | use Array::Utils qw(array_diff array_minus); 15 | use Config::IniFiles; 16 | use Data::Validate::IP qw(is_ipv4 is_ipv6); 17 | use Digest::MD5::File qw( file_md5_hex ); 18 | use File::Basename; 19 | use File::Pid; 20 | use Getopt::Long; 21 | use Log::Log4perl qw(get_logger); 22 | use Scalar::Util qw(looks_like_number); 23 | use Switch; 24 | use Time::Piece; 25 | 26 | # Default variables 27 | my $help = undef; 28 | my $check = undef; 29 | my $version = undef; 30 | my $config = '/etc/exabgp/healthcheck.conf'; 31 | my $command = 'list'; 32 | my $healthdir = '/var/healthcheck/'; 33 | 34 | # List of valid commands that can be used 35 | my @commands = ( 'list', 'announce', 'validate', 'status' ); 36 | 37 | # Parse the provided options 38 | GetOptions( 39 | "help|h" => \$help, 40 | "version|v" => \$version, 41 | "name|n=s" => \$check, 42 | "config|f=s" => \$config, 43 | "command|c=s" => \$command, 44 | ); 45 | 46 | ########## Begin Script ########## 47 | 48 | my $script_version = '0.4.4'; 49 | 50 | # Get this scripts name 51 | my $name = basename($0); 52 | my $full_name = $0; 53 | 54 | # If we need to print help, print and exit. 55 | if ($help) { print_help(); } 56 | 57 | # If we need to print the version for the script, print and exit. 58 | if ($version) { print_version(); } 59 | 60 | # Check to see if this script is running interactively. If so, let debug messages go to console 61 | my $console_debug = undef; 62 | if ($ENV{'USER'}) { $console_debug = 1; } 63 | else { $SIG{INT} = sub { }; $SIG{TERM} = \&terminate; } 64 | 65 | # Make STDOUT unbuffered 66 | select STDOUT; $| = 1; 67 | 68 | # Various variables that are used 69 | my ($cfg,$logger,$service_state,$service_metric,$service_nexthop,@service_ips,$statusfile,$pidfile,$pid,$cmd,$logcheck); 70 | 71 | # Initialise the script with some basic sanity checks 72 | init(); 73 | 74 | # Load config 75 | $cfg = Config::IniFiles->new( -file => $config ); 76 | 77 | # Decide what needs to be done 78 | switch ($command) { 79 | 80 | # Announce routes for a config 81 | case 'announce' { 82 | 83 | # Start logging 84 | $logger = start_log(); 85 | 86 | # Validate the configuration to make sure it will work for this check. 87 | if (validate_config($check) ne 'valid') { 88 | $logger->error("$check: The configuration file cannot be validated due to errors, this script will not run."); 89 | $logger->error(validate_config($check)); 90 | print "Script died due to config errors:\n"; 91 | print validate_config($check); 92 | die; 93 | } 94 | 95 | # Make sure that the status file can be opened for writing. This only matters if its running under ExaBGP and not via SSH 96 | if (! $console_debug) { 97 | $statusfile = "$healthdir$check"; 98 | $pidfile = "$healthdir$check.run"; 99 | if (-f $statusfile && ! -w $statusfile) { 100 | $logger->error("$check: Cannot write status file. Ensure that $statusfile is writable."); 101 | print "Cannot write status file or PID file. Ensure that $pidfile and $statusfile are writable.\n"; 102 | die; 103 | } 104 | # Writeout PID file 105 | $pid = File::Pid->new({file => $pidfile}); 106 | if ($pid->running()) { 107 | $logger->error("$check: Process already locked, check there isn't already a process running for this."); 108 | print "Process already locked, check there isn't already a process running for this check.\n"; 109 | die; 110 | } 111 | $pid->write(); 112 | } 113 | 114 | # Start the announcements 115 | run_announce(); 116 | } 117 | 118 | case 'list' { 119 | # If this is for a single service, get the facts 120 | if ($check) { 121 | print "Facts for $check:\n"; 122 | # Make sure config is valid 123 | if (validate_config($check) ne 'valid') { 124 | print "Error: Invalid configuration. Perhaps try $full_name -c validate -n $check\n"; 125 | } else { 126 | get_facts($check); 127 | } 128 | } else { 129 | # Get a list of sections and loop over them 130 | foreach (@{$cfg->{mysects}}) { 131 | $check = $_; 132 | # Skip the global section 133 | if ($check eq 'global') { next; } 134 | print "Facts for $check:\n"; 135 | # Make sure config is valid 136 | if (validate_config($check) ne 'valid') { 137 | print "Error: Invalid configuration. Perhaps try $full_name -c validate -n $check\n"; 138 | } else { 139 | get_facts($check); 140 | } 141 | print "\n"; 142 | } 143 | } 144 | } 145 | 146 | case 'validate' { 147 | # Check if there is a check name to validate. If there is not one all configs should be validated. 148 | if ($check) { 149 | print "Validating configuration for check $check: "; 150 | if (validate_config($check) ne 'valid') { 151 | print "Invalid\n"; 152 | print validate_config($check); 153 | } else { 154 | print "Valid\n"; 155 | } 156 | } else { 157 | # Get a list of sections and loop over them 158 | foreach (@{$cfg->{mysects}}) { 159 | $check = $_; 160 | # Skip the global section 161 | if ($check eq 'global') { next; } 162 | # Validate each section 163 | print "Validating configuration for check $check: "; 164 | if (validate_config($check) ne 'valid') { 165 | print "Invalid\n"; 166 | print validate_config($check); 167 | } else { 168 | print "Valid\n"; 169 | } 170 | } 171 | } 172 | } 173 | 174 | case 'status' { 175 | # Check if there is a service name. If not all show status for all services 176 | if ($check) { 177 | # Make sure there is a status file 178 | $statusfile = "$healthdir$check"; 179 | if (! -f $statusfile) { 180 | print "NO STATUS FILE FOR $check - PERHAPS IT HAS NOT RUN YET\n"; 181 | } else { 182 | print "STATUS FOR $check:\n"; 183 | open(STATUS, $statusfile); 184 | while () { 185 | print; 186 | } 187 | close(STATUS); 188 | } 189 | } else { 190 | # Get a list of sections and loop over them 191 | foreach (@{$cfg->{mysects}}) { 192 | $check = $_; 193 | # Skip the global section 194 | if ($check eq 'global') { next; } 195 | # Make sure there is a status file 196 | $statusfile = "$healthdir$check"; 197 | if (! -f $statusfile) { 198 | print "NO STATUS FILE FOR $check - PERHAPS IT HAS NOT RUN YET\n"; 199 | } else { 200 | print "STATUS FOR $check:\n"; 201 | open(STATUS, $statusfile); 202 | while () { 203 | print; 204 | } 205 | close(STATUS); 206 | } 207 | print "\n"; 208 | } 209 | } 210 | } 211 | 212 | } 213 | 214 | ########## Begin Subs ########## 215 | 216 | # Sub to initialise the script 217 | sub init { 218 | my $errors = undef; 219 | 220 | # Check that the configuration file exists 221 | if (! $config) { $errors .= "No configuration file was specified\n" } 222 | elsif (! -f $config) { $errors .= "Error: The configuration file specified does not exist: $config\n"; } 223 | 224 | # Make sure that a valid command was given 225 | my %commands = map { $_ => 1 } @commands; 226 | if (! exists($commands{$command})) { $errors .= "Error: An invalid command was provided: $command\n"; } 227 | 228 | # If the script is to announce routes, validate there was a check name provided 229 | if ($command eq 'announce' && ! $check) { $errors .= "Error: No check name was specified\n"; } 230 | 231 | # Verify that the healthcheck directory exists 232 | if (! -d $healthdir) { $errors .= "Error: The directory $healthdir does not exist\n"; } 233 | elsif (! -w $healthdir) { $errors .= "Error: The directory $healthdir is not writable\n"; } 234 | 235 | # Check to see if any errors were returned. If they were, print help. 236 | if ($errors) { print_help($errors); } 237 | } 238 | 239 | # Sub to get facts about a service 240 | sub get_facts { 241 | my $check_name = shift; 242 | 243 | print " - Service Name: $check_name\n"; 244 | print " - Check Command: " . get_value('command', $check_name) . "\n"; 245 | print " - Check Interval: " . get_value('interval', $check_name) . "\n"; 246 | print " - Check Timeout: " . get_value('timeout', $check_name) . "\n"; 247 | print " - Disable File: " . get_value('disable', $check_name) . "\n"; 248 | print " - Service Rise: " . get_value('rise', $check_name) . "\n"; 249 | print " - Service Fall: " . get_value('fall', $check_name) . "\n"; 250 | print " - Service Log: " . get_value('logfile', $check_name) . "\n"; 251 | print " - Debug Log: " . get_value('debug', $check_name) . "\n"; 252 | print " - Log Check Output: " . get_value('logcheck', $check_name) . "\n"; 253 | print " - Route Metric: " . get_value('metric', $check_name) . "\n"; 254 | print " - Route Nexthop: " . get_value('nexthop', $check_name) . "\n"; 255 | print " - Announce IP's:\n"; 256 | my @ips = get_value('ip', $check_name); 257 | foreach (@ips) { 258 | print " - $_\n"; 259 | } 260 | } 261 | 262 | # Sub to run check commands 263 | sub run_announce { 264 | 265 | # Check if the service has been disabled 266 | my $service_disabled; 267 | if (-f get_value('disable')) { 268 | $service_disabled = 'yes'; 269 | # Set the process status 270 | proc_status('DISABLED'); 271 | } else { 272 | $service_disabled = 'no'; 273 | # Set the process status 274 | proc_status('INIT'); 275 | } 276 | 277 | # Set the service state to down initially 278 | $service_state = 'down'; 279 | 280 | # Get a list of service IP's and put in array 281 | @service_ips = get_value('ip'); 282 | 283 | # Get the metric to announce 284 | $service_metric = get_value('metric'); 285 | 286 | # Set the rise/fall values initially 287 | my $service_rise = 0; 288 | my $service_fall = 0; 289 | 290 | # Get the configured values for rise/fall 291 | my $service_rise_req = get_value('rise'); 292 | my $service_fall_req = get_value('fall'); 293 | 294 | # Get the log path 295 | my $log_path = get_value('logfile'); 296 | 297 | # Get the debug option 298 | my $service_debug = get_value('debug'); 299 | 300 | # Get the check interval 301 | my $interval = get_value('interval'); 302 | 303 | # Get the check command. This will be prepended with the "timeout" utility for executing the check. 304 | my $command = get_value('command'); 305 | my $timeout = get_value('timeout'); 306 | 307 | # The check command needs any quoting removed from start/end 308 | my $command_run = $command; 309 | $command_run =~ s/^"//; $command_run =~ s/"$//; 310 | $cmd = "timeout $timeout $command_run"; 311 | 312 | # Should we log the check output 313 | $logcheck = get_value('logcheck'); 314 | 315 | # Last result variable 316 | my $last_result = undef; 317 | 318 | # Get the next hop address 319 | $service_nexthop = get_value('nexthop'); 320 | 321 | # Get the current hash of the config file to check for changes 322 | my $config_md5 = file_md5_hex($config); 323 | my $new_config_md5 = $config_md5; 324 | 325 | # Config is valid 326 | my $config_valid = 'valid'; 327 | 328 | # Start loop 329 | while (1) { 330 | 331 | # Set start time 332 | my $start = time(); 333 | 334 | $logger->debug("$check: Check start"); 335 | 336 | # Check the hash of the config file only if it exists still. 337 | 338 | if (-f $config) { $new_config_md5 = file_md5_hex($config); } 339 | else { $logger->info("$check: The configuration file has disappeared?"); } 340 | 341 | # If the config has has changed since last run, reload the config, validated it and use the new values. 342 | if ($new_config_md5 ne $config_md5) { 343 | # File has changed, re-read and validate config 344 | $logger->debug("$check: Configuration file has changed since last check, reloading and validating config"); 345 | $cfg->ReadConfig; 346 | $config_valid = validate_config($check); 347 | if ($config_valid ne 'valid') { 348 | $logger->error("$check: Configuration file is not valid. Not reloading any changes. Error: $config_valid"); 349 | } else { 350 | # Check the log path is still the same 351 | if (get_value('logfile') ne $log_path || get_value('debug') ne $service_debug) { 352 | # Restart logger due to config change 353 | $log_path = get_value('logfile'); 354 | $service_debug = get_value('debug'); 355 | $logger = start_log(); 356 | } 357 | 358 | # Check if the interval for checks has been changed 359 | if (get_value('interval') ne $interval) { 360 | $logger->info("$check: Check interval has been changed from $interval to ".get_value('interval')); 361 | $interval = get_value('interval'); 362 | } 363 | 364 | # Check if the option to log check output has been changed 365 | if (get_value('logcheck') ne $logcheck) { 366 | $logger->info("$check: Check logging has changed from $logcheck to ".get_value('logcheck')); 367 | $logcheck = get_value('logcheck'); 368 | } 369 | 370 | # Check if the values for rise/fall have changed 371 | if (get_value('rise') ne $service_rise_req) { 372 | $logger->info("$check: Check rise value has changed from $service_rise_req to ".get_value('rise')); 373 | $service_rise_req = get_value('rise'); 374 | } 375 | if (get_value('fall') ne $service_fall_req) { 376 | $logger->info("$check: Check fall value has changed from $service_fall_req to ".get_value('fall')); 377 | $service_fall_req = get_value('fall'); 378 | } 379 | 380 | # Check if the timeout or check command has been changed. 381 | if (get_value('timeout') ne $timeout || get_value('command') ne $command) { 382 | if (get_value('timeout') ne $timeout) { $logger->info("$check: Check timeout has been changed from $timeout to ".get_value('timeout')); $timeout = get_value('timeout'); } 383 | if (get_value('command') ne $command) { $logger->info("$check: Check command has been changed from $command to ".get_value('command')); $command = get_value('command'); } 384 | # The check command needs any quoting removed from start/end 385 | $command_run = $command; 386 | $command_run =~ s/^"//; $command_run =~ s/"$//; 387 | $cmd = "timeout $timeout $command_run"; 388 | } 389 | 390 | # Define the variable $changes - this will be set if there are changes that require all routes to be withdrawn and announced again. 391 | my $changes; 392 | 393 | # Check if the metric has changed 394 | if ($service_metric ne get_value('metric')) { 395 | $logger->info("$check: Metric for routes has changed from $service_metric to ".get_value('metric')); 396 | $changes = 1; 397 | } 398 | 399 | # Check if the nexthop has changed 400 | if ($service_nexthop ne get_value('nexthop')) { 401 | $logger->info("$check: Nexthop address has changed from $service_nexthop to ".get_value('nexthop')); 402 | $changes = 1; 403 | } 404 | 405 | # Check if the list of IP's to announce has changed 406 | my @new_service_ips = get_value('ip'); 407 | if (array_diff(@new_service_ips, @service_ips)) { 408 | # The list of IP's to announce has changed and needs to be handled. 409 | # Print out a log entry to keep a record of the list of IP's changing 410 | my $new_ips_csv = join(',', @new_service_ips); 411 | my $old_ips_csv = join(',', @service_ips); 412 | $logger->info("$check: IP list changed. Old IP's: $old_ips_csv. New IP's: $new_ips_csv."); 413 | # If there are other changes to be made, there is nothing to do - all routes will be withdrawn anyway. 414 | # If there are no other changes to make we only need to withdraw the routes for the IP's that are no longer configured and announce the routes for the new IP's that have been configured. 415 | if (! $changes) { 416 | # First handle the IP's to remove. 417 | my @to_remove = array_minus( @service_ips, @new_service_ips ); 418 | if (@to_remove) { 419 | $old_ips_csv = join(',', @to_remove); 420 | $logger->debug("$check: Running withdraw routes for the following IP's as they have been removed from the configuration: $old_ips_csv"); 421 | withdraw_ips($old_ips_csv); 422 | } 423 | # Handle any new IP's to announce. 424 | my @to_announce = array_minus( @new_service_ips, @service_ips ); 425 | if (@to_announce) { 426 | $old_ips_csv = join(',', @to_announce); 427 | $logger->debug("$check: Running announce routes for the following IP's as they have been added to the configuration: $old_ips_csv"); 428 | announce_ips($old_ips_csv); 429 | } 430 | # Update @service_ips with the new list of IP's 431 | @service_ips = get_value('ip'); 432 | } 433 | } 434 | 435 | # Check if there are changes to be made. 436 | if ($changes) { 437 | # If the service is currently in the UP status, withdraw the routes. 438 | if ($service_state eq 'up') { 439 | $logger->info("$check: All routes are being withdrawn due to metric or nexthop change"); 440 | withdraw_ips(); 441 | } 442 | # Update the configuration settings 443 | @service_ips = get_value('ip'); 444 | $service_metric = get_value('metric'); 445 | $service_nexthop = get_value('nexthop'); 446 | # Announce the routes again using the new configuration values 447 | if ($service_state eq 'up') { 448 | $logger->info("$check: Previously withdrawn routes are being announced again due to metric or nexthop change"); 449 | announce_ips(); 450 | } 451 | } 452 | 453 | $logger->info("$check: Configuration file reload complete"); 454 | } 455 | # Update hash for the config file. 456 | $config_md5 = $new_config_md5; 457 | } 458 | 459 | # Check if the service has been disabled. 460 | if (-f get_value('disable')) { 461 | # Check if this is a state change 462 | if ($service_disabled ne 'yes') { 463 | # Disabled file has been created since last check. First, check to see if the service is currently up. If the services is up, withdraw the routes. 464 | $logger->info("$check: Service has been disabled by file check. No further service checks will run until this is removed."); 465 | if ($service_state eq 'up') { 466 | $logger->debug("$check: Withdrawing IP's and setting service to down due to service being disabled"); 467 | withdraw_ips(); 468 | $service_state = 'down'; 469 | } 470 | $service_disabled = 'yes'; 471 | # Set the process status 472 | proc_status('DISABLED'); 473 | } 474 | } else { 475 | # Service shouldn't be disabled. Check to see if it was previously 476 | if ($service_disabled eq 'yes') { 477 | $logger->info("$check: Service was previously disabled by file check. Setting back to enabled. Service checks need to pass before routes are announced."); 478 | $service_disabled = 'no'; 479 | # Ensure that service state is down to reset all counters 480 | $service_state = 'down'; 481 | # Set the process status 482 | proc_status('INIT'); 483 | } 484 | } 485 | 486 | # Only run checks if the service is not disabled 487 | if ($service_disabled eq 'no') { 488 | # Run the check 489 | my $result = run_check(); 490 | 491 | # Check if $last_result is defined. If not, define it. 492 | if (! $last_result) { 493 | $last_result = $result; 494 | } else { 495 | # Compare last_result to the result. 496 | if ($last_result ne $result) { 497 | 498 | # If the last result was != 0 (fail) but the current result is 0 (success), reset the fall counter 499 | # The process name needs to be updated otherwise it will still be in the falling state when checking the process list 500 | if ($last_result ne 0 && $result eq 0) { 501 | $service_fall = 0; 502 | proc_status('UP'); 503 | } 504 | 505 | # If the last result was 0 (success) but the current result is != 0 (fail), reset the rise counter 506 | # The process name needs to be updated otherwise it will still be in the rising state when checking the process list 507 | if ($last_result eq 0 && $result ne 0) { 508 | $service_rise = 0; 509 | proc_status('DOWN'); 510 | } 511 | 512 | } 513 | 514 | # Store the last result 515 | $last_result = $result; 516 | } 517 | 518 | # Check if the service check returned 0 (success) 519 | if ($result eq 0) { 520 | 521 | # Check if the service is currently marked down. If so, increment $service_rise. 522 | if ($service_state eq 'down') { 523 | $service_rise++; 524 | # Check if the value of $service_rise is high enough to mark the service as up 525 | if ($service_rise >= $service_rise_req) { 526 | $logger->info("$check: Last check succeeded. Service has met the number of success checks required, marking as up and announcing IP's"); 527 | # Service should be marked as up. 528 | $service_state = 'up'; 529 | # Announce routes 530 | announce_ips(); 531 | # Reset the $service_rise value 532 | $service_rise = 0; 533 | # Set the process status 534 | proc_status('UP'); 535 | } else { 536 | my $service_rise_left = $service_rise_req - $service_rise; 537 | $logger->info("$check: Last check succeeded. Service needs $service_rise_left checks to succeed before it is active"); 538 | # Set the process status 539 | proc_status("DOWN | RISING $service_rise/$service_rise_req"); 540 | } 541 | } 542 | 543 | } else { 544 | 545 | # The service is down, check to see if the service is up. If so, increment $service_fall. 546 | if ($service_state eq 'up') { 547 | $service_fall++; 548 | # Check if the value of $service_fall is high enough to mark the service as down 549 | if ($service_fall >= $service_fall_req) { 550 | $logger->info("$check: Last check failed. Service has met the number of failure checks required, marking service as down and withdrawing IP's"); 551 | # Service should be marked as down 552 | $service_state = 'down'; 553 | # Withdraw routes 554 | withdraw_ips(); 555 | # Reset the $service_fall value 556 | $service_fall = 0; 557 | # Set the process status 558 | proc_status('DOWN'); 559 | } else { 560 | my $service_fall_left = $service_fall_req - $service_fall; 561 | $logger->info("$check: Last check failed. Service needs $service_fall_left checks to fail before it is down"); 562 | # Set the process status 563 | proc_status("UP | FALLING $service_fall/$service_fall_req"); 564 | } 565 | } 566 | 567 | } 568 | } 569 | 570 | # Check how long this check took, sleep for the appropriate amount of time to start next check 571 | my $end = time(); 572 | my $runtime = $end - $start; 573 | my $sleeptime = $interval - $runtime; 574 | # Minimum sleep period is 1 second to prevent a very tight loop 575 | if ($sleeptime lt 0) { 576 | $sleeptime = 1; 577 | } 578 | $logger->debug("$check: Check complete. Sleeping $sleeptime seconds before next check"); 579 | sleep $sleeptime; 580 | 581 | } 582 | 583 | } 584 | 585 | # Sub to validate config file for a section 586 | sub validate_config { 587 | my $section = shift; 588 | my $errors = undef; 589 | 590 | # Ensure that there is a section for global config 591 | if (! $cfg->SectionExists('global') ) { return "No configuration for the global section\n"; } 592 | 593 | # Ensure that there is a section for this config name 594 | if (! $cfg->SectionExists($section) ) { return "No configuration for the $section section\n"; } 595 | 596 | # Ensure there is a log file 597 | if (! get_value('logfile',$section)) { $errors .= " - No log file specified\n"; } 598 | 599 | # Ensure there is a metric set and it is valid 600 | if (! get_value('metric',$section)) { $errors .= " - No metric specified\n"; } 601 | elsif (! looks_like_number(get_value('metric',$section))) { $errors .= " - Metric specified is not a number\n"; } 602 | elsif (get_value('metric',$section) < 1 || get_value('metric',$section) > 1000) { $errors .= " - Metric specified must be between 1 and 1000\n"; } 603 | 604 | # Ensure there is a check interval set and it is valid 605 | if (! get_value('interval',$section)) { $errors .= " - No check interval specified\n"; } 606 | elsif (! looks_like_number(get_value('interval',$section))) { $errors .= " - Check interval specified is not a number\n"; } 607 | 608 | # Ensure there is a check timeout set 609 | if (! get_value('timeout',$section)) { $errors .= " - No timeout value specified\n"; } 610 | elsif (! looks_like_number(get_value('timeout',$section))) { $errors .= " - Timeout specified is not a number\n"; } 611 | 612 | # Ensure that the check timeout is less than the check interval 613 | if (get_value('timeout',$section) > get_value('interval',$section)) { $errors .= " - The timeout specified is larger than the check interval\n"; } 614 | 615 | # Ensure that there is a rise value set and it is valid 616 | if (! get_value('rise',$section)) { $errors .= " - No rise value specified\n"; } 617 | elsif (! looks_like_number(get_value('rise',$section))) { $errors .= " - Rise value specified is not a number\n"; } 618 | 619 | # Ensure that there is a fall value set and it is valid 620 | if (! get_value('fall',$section)) { $errors .= " - No fall value specified\n"; } 621 | elsif (! looks_like_number(get_value('fall',$section))) { $errors .= " - Fall value specified is not a number\n"; } 622 | 623 | # Ensure there is a logcheck value 624 | if (! get_value('logcheck',$section)) { $errors .= " - No logcheck value specified\n"; } 625 | elsif (get_value('logcheck',$section) ne 'yes' && get_value('logcheck',$section) ne 'no') { $errors .= " - Logcheck value must be yes or no\n"; } 626 | 627 | # Ensure there is a logcheck value 628 | if (! get_value('debug',$section)) { $errors .= " - No debug value specified\n"; } 629 | elsif (get_value('debug',$section) ne 'yes' && get_value('debug',$section) ne 'no') { $errors .= " - Debug value must be yes or no\n"; } 630 | 631 | # Ensure there is a check command 632 | if (! get_value('command',$section)) { $errors .= " - No check command specified\n"; } 633 | 634 | # Next hop IP 635 | my $nexthop_family; 636 | if (! get_value('nexthop',$section)) { $errors .= " - Next hop IP address not supplied\n"; } 637 | else { 638 | my $nexthop = get_value('nexthop',$section); 639 | # Ensure next hop IP is valid 640 | if (is_ipv4($nexthop)) { 641 | $nexthop_family = '4'; 642 | } elsif (is_ipv6($nexthop)) { 643 | $nexthop_family = '6'; 644 | } else { 645 | $errors .= " - Next hop IP address not valid. It should be an IPv4 or IPv6 address.\n"; 646 | } 647 | } 648 | 649 | # Ensure there is at least one IP address supplied 650 | if (! get_value('ip',$section)) { $errors .= " - No IP addresses to advertise\n"; } 651 | else { 652 | # We can't validate the IP's unless $nexthop_family has been set. 653 | if (! $nexthop_family) { $errors .= " - IP address validation skipped due to nexthop configuration error\n"; } 654 | else { 655 | # Ensure that the IP addresses are in a valid and that they are in the correct address family. 656 | my @ips = get_value('ip',$section); 657 | foreach my $ip (@ips) { 658 | # If the IP's are not in the format x.x.x.x/x or ::1/x, validate them 659 | if ($ip !~ m/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2}/ && $ip !~ m/[A-Za-z0-9:]+\/\d{1,3}/) { 660 | if ($nexthop_family eq '4') { if (! is_ipv4($ip)) { $errors .= " - IP address $ip is not not a valid IPv4 address\n"; } } 661 | if ($nexthop_family eq '6') { if (! is_ipv6($ip)) { $errors .= " - IP address $ip is not not a valid IPv6 address\n"; } } 662 | if (get_value('nexthop',$section) eq $ip) { $errors .= " - IP address to advertise ($ip) cannot be the same as the nexthop\n"; } 663 | } 664 | # The IP's are in the format x.x.x.x/x or ::1/x. Split the string into $address and $mask, then validate those. 665 | else { 666 | my ($address, $mask) = split('/', $ip); 667 | # Validations for IPv4 668 | if ($nexthop_family eq '4') { 669 | # Validate mask is between 1 and 32 670 | if (! looks_like_number($mask) || $mask < 1 || $mask > 32) { $errors .= " - Netmask for address $ip must be between 1 and 32\n"; } 671 | if (! is_ipv4($address)) { $errors .= " - IP address $ip is not a valid IPv4 address\n"; } 672 | elsif ($mask eq '32' && get_value('nexthop',$section) eq $address) { $errors .= " - IP address to advertise ($ip) cannot be the same as the nexthop\n"; } 673 | } 674 | # Validations for IPv6 675 | if ($nexthop_family eq '6') { 676 | # Validate mask is between 1 and 128 677 | if (! looks_like_number($mask) || $mask < 1 || $mask > 128) { $errors .= " - Netmask for address $ip must be between 1 and 128\n"; } 678 | if (! is_ipv6($address)) { $errors .= " - IP address $ip is not a valid IPv6 address\n"; } 679 | elsif ($mask eq '128' && get_value('nexthop',$section) eq $address) { $errors .= " - IP address to advertise ($ip) cannot be the same as the nexthop\n"; } 680 | } 681 | } 682 | } 683 | } 684 | } 685 | 686 | # Disable file 687 | if (! get_value('disable',$section)) { $errors .= " - No disable file specified\n"; } 688 | 689 | # Ensure that the script can do logging 690 | my $logfile = get_value('logfile',$section); 691 | my $logdir = dirname($logfile); 692 | if (! -d $logdir) { 693 | mkdir $logdir or $errors .= " - Could not create log directory $logdir: $!\n"; 694 | # This script will probably be running as a different user than it will be with ExaBGP if it is being run on the command line so delete the directory and let ExaBGP create it 695 | if (-d $logdir && $console_debug) { rmdir $logdir; } 696 | } else { 697 | if (-f $logfile && ! -w $logfile) { $errors .= " - Could not write to log file $logfile\n"; } 698 | elsif (! -f $logfile) { 699 | open my $LOGFH, '>', "$logfile" or $errors .= " - Could not create log file $logfile: $!\n"; 700 | if ($LOGFH) { 701 | close $LOGFH; 702 | } 703 | if (-f $logfile && $console_debug) { unlink $logfile; } 704 | } 705 | } 706 | 707 | # Checks complete 708 | if ($errors) { 709 | return $errors; 710 | } else { 711 | return 'valid'; 712 | } 713 | 714 | } 715 | 716 | # Sub to get a configuration value. 717 | sub get_value { 718 | my $key = shift; 719 | my $section = shift // $check; 720 | 721 | if ($cfg->exists($section, $key)) { 722 | return $cfg->val($section, $key); 723 | } elsif ($cfg->exists('global', $key)) { 724 | return $cfg->val('global', $key); 725 | } else { 726 | return undef; 727 | } 728 | } 729 | 730 | # Sub that executes when the script terminates 731 | sub terminate { 732 | # Cleanup only needs to be done if the script was announcing and running under ExaBGP 733 | if (! $console_debug && $command eq 'announce') { 734 | proc_status('TERMINATED'); 735 | $pid->remove(); 736 | } 737 | exit; 738 | } 739 | 740 | # Sub to write out status file 741 | sub update_status_file { 742 | my $status = shift; 743 | 744 | my $current_time = localtime(time())->strftime('%F %T'); 745 | 746 | open STATUSFH, '>', $statusfile; 747 | print STATUSFH "Service State: $status\n"; 748 | print STATUSFH "Last State Change: $current_time\n"; 749 | if (! defined $service_nexthop) { print STATUSFH "Nexthop: ".get_value('nexthop')."\n"; } 750 | else { print STATUSFH "Nexthop: $service_nexthop\n"; } 751 | print STATUSFH "Managed IP's: "; 752 | foreach my $ip (@service_ips) { 753 | print STATUSFH "$ip "; 754 | } 755 | print STATUSFH "\n"; 756 | close STATUSFH 757 | } 758 | 759 | # Sub to handle the process status 760 | sub proc_status { 761 | my $status = shift; 762 | 763 | # This should only be called in announcement mode when running under exabgp. 764 | if (! $console_debug && $command eq 'announce') { 765 | 766 | # Set the name that shows in ps 767 | $0 = "ExaBGP $name: $check $status"; 768 | 769 | # Write out status file 770 | update_status_file($status); 771 | } 772 | } 773 | 774 | # Sub to announce all IP's 775 | sub announce_ips { 776 | my $to_announce = shift || undef; 777 | my @announce_list; 778 | 779 | # Ensure the service_state is up before announcing anything 780 | if ($service_state eq 'up') { 781 | # If a list of IP's to announce was provided, announce those otherwise announce all IP's in @service_ips. 782 | if ($to_announce) { @announce_list = split(',', $to_announce); } 783 | else { @announce_list = @service_ips; } 784 | foreach my $ip (@announce_list) { 785 | # If there was no mask specified, it will default to /32 for IPv4 or /128 for IPv6 786 | if ($ip !~ m/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2}/ && $ip !~ m/[A-Za-z0-9:]+\/\d{1,3}/) { 787 | if (is_ipv4($ip)) { $ip = "$ip/32"; } 788 | if (is_ipv6($ip)) { $ip = "$ip/128"; } 789 | } 790 | my $announce = "announce route $ip next-hop $service_nexthop med $service_metric"; 791 | $logger->debug("$check: Sending to exabgp: $announce"); 792 | print "$announce\n"; 793 | } 794 | } 795 | } 796 | 797 | # Sub to withdraw all IP's 798 | sub withdraw_ips { 799 | my $to_withdraw = shift || undef; 800 | my @withdraw_list; 801 | 802 | # If a list of IP's to withdraw was provided, withdraw those otherwise withdraw all IP's in @service_ips. 803 | if ($to_withdraw) { @withdraw_list = split(',', $to_withdraw); } 804 | else { @withdraw_list = @service_ips; } 805 | foreach my $ip (@withdraw_list) { 806 | # If there was no mask specified, it will default to /32 for IPv4 or /128 for IPv6 807 | if ($ip !~ m/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2}/ && $ip !~ m/[A-Za-z0-9:]+\/\d{1,3}/) { 808 | if (is_ipv4($ip)) { $ip = "$ip/32"; } 809 | if (is_ipv6($ip)) { $ip = "$ip/128"; } 810 | } 811 | my $announce = "withdraw route $ip next-hop $service_nexthop med $service_metric"; 812 | $logger->debug("$check: Sending to exabgp: $announce"); 813 | print "$announce\n"; 814 | } 815 | } 816 | 817 | # Sub to execute check command 818 | sub run_check { 819 | # Remove quotes from start/end of the command if it is quoted 820 | if ($cmd =~ /^"/) { 821 | $cmd =~ s/^"//; 822 | $cmd =~ s/"$//; 823 | } 824 | 825 | $logger->debug("$check: Attempting to fork and run check command [$cmd]"); 826 | 827 | # opening a pipe creates a forked process 828 | my $pid = open(my $pipe, '-|'); 829 | $logger->error("$check: Cannot fork, service marked down: $!") unless defined $pid; 830 | return(-1) unless defined $pid; 831 | 832 | if ($pid) { 833 | my @result = (); 834 | @result = <$pipe>; 835 | close($pipe); 836 | my $exit_code = $?; 837 | $logger->debug("$check: Executed check command [$cmd]. Return code [$exit_code]"); 838 | if ($logcheck eq 'yes') { 839 | $logger->debug("$check: Output from [$cmd]: @result"); 840 | } 841 | return ($exit_code); 842 | } 843 | 844 | { 845 | no warnings; 846 | open(STDERR, '>&STDOUT'); 847 | ref($cmd) eq 'ARRAY' ? exec(@$cmd) : exec($cmd); 848 | } 849 | $logger->error("$check: Cannot exec command [@$cmd]: $!"); 850 | return (-2); 851 | } 852 | 853 | # Sub to start logging 854 | sub start_log { 855 | my $log_path = get_value('logfile'); 856 | 857 | my $rootlogger; 858 | 859 | # If this is running with -debug passed to the script, only log to screen. 860 | if ($console_debug) { 861 | $rootlogger = 'DEBUG, Screen'; 862 | } else { 863 | if (get_value('debug') eq 'yes') { 864 | $rootlogger = 'DEBUG, AppInfo, AppError, AppDebug'; 865 | } else { 866 | $rootlogger = 'DEBUG, AppInfo, AppError'; 867 | } 868 | } 869 | 870 | my $log_conf = " 871 | log4perl.rootLogger = $rootlogger 872 | 873 | log4perl.appender.AppInfo = Log::Dispatch::FileRotate 874 | log4perl.appender.AppInfo.filename = $log_path 875 | log4perl.appender.AppInfo.mode = append 876 | log4perl.appender.AppInfo.autoflush = 1 877 | log4perl.appender.AppInfo.size = 10485760 878 | log4perl.appender.AppInfo.max = 10 879 | log4perl.appender.AppInfo.layout = Log::Log4perl::Layout::PatternLayout 880 | log4perl.appender.AppInfo.recreate = 1 881 | log4perl.appender.AppInfo.Threshold = INFO 882 | log4perl.appender.AppInfo.layout.ConversionPattern = %d %P %p %m %n 883 | 884 | log4perl.appender.AppDebug = Log::Dispatch::FileRotate 885 | log4perl.appender.AppDebug.filename = $log_path.debug 886 | log4perl.appender.AppDebug.mode = append 887 | log4perl.appender.AppDebug.autoflush = 1 888 | log4perl.appender.AppDebug.size = 10485760 889 | log4perl.appender.AppDebug.max = 10 890 | log4perl.appender.AppDebug.layout = Log::Log4perl::Layout::PatternLayout 891 | log4perl.appender.AppDebug.recreate = 1 892 | log4perl.appender.AppDebug.Threshold = DEBUG 893 | log4perl.appender.AppDebug.layout.ConversionPattern = %d %P %p %m %n 894 | 895 | log4perl.appender.AppError = Log::Dispatch::FileRotate 896 | log4perl.appender.AppError.filename = $log_path.error 897 | log4perl.appender.AppError.mode = append 898 | log4perl.appender.AppError.autoflush = 1 899 | log4perl.appender.AppError.size = 10485760 900 | log4perl.appender.AppError.max = 10 901 | log4perl.appender.AppError.layout = Log::Log4perl::Layout::PatternLayout 902 | log4perl.appender.AppError.recreate = 1 903 | log4perl.appender.AppError.Threshold = ERROR 904 | log4perl.appender.AppError.layout.ConversionPattern = %d %P %p %m %n 905 | 906 | log4perl.appender.Screen = Log::Log4perl::Appender::Screen 907 | log4perl.appender.Screen.stderr = 0 908 | log4perl.appender.Screen.layout = Log::Log4perl::Layout::PatternLayout 909 | log4perl.appender.Screen.Threshold = DEBUG 910 | log4perl.appender.Screen.layout.ConversionPattern = %d %P %p %m %n 911 | "; 912 | Log::Log4perl::init(\$log_conf); 913 | 914 | my $logger = Log::Log4perl->get_logger(); 915 | return $logger; 916 | } 917 | 918 | # Sub to print out version info 919 | sub print_version { 920 | print <