#!/usr/bin/env python # # Copyright Gareth Bowles 2010 # # Based on the check_svn plugin written by Hari Sekhon # # 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. # # 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. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # """Nagios plugin to check the current EC2 instance spot price. Requires the Amazon EC2 command line tools to be installed somewhere in the path""" # Standard Nagios return codes OK = 0 WARNING = 1 CRITICAL = 2 UNKNOWN = 3 import datetime import os import re import sys import signal import time try: from subprocess import Popen, PIPE, STDOUT except ImportError: print "UNKNOWN: Failed to import python subprocess module.", print "Perhaps you are using a version of python older than 2.4?" sys.exit(CRITICAL) from optparse import OptionParser __author__ = "Gareth Bowles" __title__ = "Nagios Plugin for Amazon EC2 Spot Price" __version__ = 0.1 DEFAULT_TIMEOUT = 20 def end(status, message): """Prints a message and exits. First arg is the status code Second Arg is the string message""" check_name = "EC2-SPOT-PRICE " if status == OK: print "%sOK: %s" % (check_name, message) sys.exit(OK) elif status == WARNING: print "%sWARNING: %s" % (check_name, message) sys.exit(WARNING) elif status == CRITICAL: print "%sCRITICAL: %s" % (check_name, message) sys.exit(CRITICAL) else: # This one is intentionally different print "UNKNOWN: %s" % message sys.exit(UNKNOWN) class SpotPriceTester: """Holds state for the EC2 spot price check""" def __init__(self): """Initializes all variables to their default states""" self.warning = "" self.critical = "" self.instance_type = "" self.os_platform = "Linux/UNIX" self.timeout = DEFAULT_TIMEOUT self.verbosity = 0 self.BIN = "" try: self.ec2_home = os.environ["EC2_HOME"] except KeyError: self.vprint(3, "Environment variable EC2_HOME not set, using value passed on command line") try: self.ec2_cert = os.environ["EC2_CERT"] except KeyError: self.vprint(3, "Environment variable EC2_CERT not set, using value passed on command line") try: self.ec2_private_key = os.environ["EC2_PRIVATE_KEY"] except KeyError: self.vprint(3, "Environment variable EC2_PRIVATE_KEY not set, using value passed on command line") try: self.java_home = os.environ["JAVA_HOME"] except KeyError: self.vprint(3, "Environment variable JAVA_HOME not set, using value passed on command line") def validate_variables(self): """Runs through the validation of all test variables Should be called before the main test to perform a sanity check on the environment and settings""" self.validate_warning() self.validate_critical() self.validate_instance_type() self.validate_os_platform() self.validate_timeout() self.verify_check_command() def validate_warning(self): """Exits with an error if the warning price level does not conform to expected format""" if self.warning == None: end(UNKNOWN, "You must supply a warning value " \ + "See --help for details") self.warning = self.warning.strip() # Input Validation - re_warning = re.compile("^\d\.\d+$") if not re_warning.match(self.warning): end(UNKNOWN, "Warning value given does not appear to be a valid " \ + "number - use a dollar value e.g. 0.03 for 3 cents") def validate_critical(self): """Exits with an error if the critical price level does not conform to expected format""" if self.critical == None: end(UNKNOWN, "You must supply a critical value " \ + "See --help for details") self.critical = self.critical.strip() # Input Validation - re_critical = re.compile("^\d\.\d+$") if not re_critical.match(self.critical): end(UNKNOWN, "Critical value given does not appear to be a valid " \ + "number - use a dollar value e.g. 0.03 for 3 cents") def validate_instance_type(self): """Validates the EC2 instance type""" if self.instance_type == None: end(UNKNOWN, "You must supply a valid EC2 instance type " \ + "See --help for details") self.instance_type = self.instance_type.strip() re_instance_type = re.compile("^[cm][12]\.\w+$") if not re_instance_type.match(self.instance_type): end(UNKNOWN, "You must supply a valid EC2 instance type " \ + "See --help for details") def validate_os_platform(self): """Exits with an error if the O/S platform is not valid""" self.os_platform = self.os_platform.strip() if self.os_platform == None: self.os_platform = "Linux/UNIX" if self.os_platform != "Linux/UNIX": if self.os_platform != "Windows": end(UNKNOWN, "O/S platform must be \"Linux/UNIX\" or \"Windows\"") def validate_timeout(self): """Exits with an error if the timeout is not valid""" if self.timeout == None: self.timeout = DEFAULT_TIMEOUT try: self.timeout = int(self.timeout) if not 1 <= self.timeout <= 65535: end(UNKNOWN, "timeout must be between 1 and 3600 seconds") except ValueError: end(UNKNOWN, "timeout number must be a whole number between " \ + "1 and 3600 seconds") if self.verbosity == None: self.verbosity = 0 def verify_check_command(self): """ Ensures the ec2-describe-spot-price-history command exists and is executable """ check_command = self.ec2_home + "/bin/ec2-describe-spot-price-history" self.vprint(3, "verify_check_command: Check command is " + check_command) self.BIN = self.check_executable(check_command) if not self.BIN: end(UNKNOWN, "The EC2 command 'ec2-describe-spot-price-history' cannot be found in your path. Please check that " \ + " you have the Amazon EC2 command line tools installed and that the EC2 environment variables are set " \ + "correctly (see http://docs.amazonwebservices.com/AWSEC2/latest/CommandLineReference/) ") # Pythonic version of "which" # def check_executable(self, file): """Takes an executable path as a string and tests if it is executable. Returns the full path of the executable if it is executable, or None if not""" self.vprint(3, "check_executable: Check command is " + file) if os.path.isfile(file): self.vprint(3, "check_executable: " + file + " is a file") if os.access(file, os.X_OK): self.vprint(3, "check_executable: " + file + " is executable") return file else: #print >> sys.stderr, "Warning: '%s' in path is not executable" self.vprint(3, "check_executable: " + file + "is not executable" ) end(UNKNOWN, "EC2 utility '%s' is not executable" % file) self.vprint(3, "check_executable: Check command " + file+ " not found ") return None def run(self, cmd): """runs a system command and returns a tuple containing the return code and the output as a single text block""" if cmd == "" or cmd == None: end(UNKNOWN, "Internal python error - " \ + "no cmd supplied for run function") self.vprint(3, "running command: %s" % cmd) local_env = os.environ.copy() local_env["EC2_HOME"] = self.ec2_home local_env["JAVA_HOME"] = self.java_home try: process = Popen( cmd.split(), bufsize=0, shell=False, stdin=PIPE, stdout=PIPE, stderr=STDOUT, env=local_env ) except OSError, error: error = str(error) if error == "No such file or directory": end(UNKNOWN, "Cannot find utility '%s'" % cmd.split()[0]) else: end(UNKNOWN, "Error trying to run utility '%s' - %s" \ % (cmd.split()[0], error)) stdout, stderr = process.communicate() if stderr == None: pass if stdout == None or stdout == "": end(UNKNOWN, "No output from utility '%s'" % cmd.split()[0]) returncode = process.returncode self.vprint(3, "Returncode: '%s'\nOutput: '%s'" % (returncode, stdout)) return (returncode, str(stdout)) def set_timeout(self): """Sets an alarm to time out the test""" if self.timeout == 1: self.vprint(2, "setting plugin timeout to 1 second") else: self.vprint(2, "setting plugin timeout to %s seconds"\ % self.timeout) signal.signal(signal.SIGALRM, self.sighandler) signal.alarm(self.timeout) def sighandler(self, discarded, discarded2): """Function to be called by signal.alarm to kill the plugin""" # Nop for these variables discarded = discarded2 discarded2 = discarded if self.timeout == 1: timeout = "(1 second)" else: timeout = "(%s seconds)" % self.timeout end(CRITICAL, "svn plugin has self terminated after exceeding " \ + "the timeout %s" % timeout) def test_spot_price(self): """Performs the EC2 spot price check""" self.validate_variables() self.set_timeout() dtstring = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S") cmd = self.BIN + " -K " + self.ec2_private_key + " -C " + self.ec2_cert + " -t " + self.instance_type \ + " -d " + self.os_platform + " -s " + dtstring self.vprint(2, "now running EC2 spot price check:\n" + cmd) result, output = self.run(cmd) if result == 0: if len(output) == 0: return (WARNING, "Check passed but no output was received " \ + "from ec2-describe-spot-price-history command, abnormal condition, " \ + "please check.") else: stripped_output = output.replace("\n", " ").rstrip(" ") price = stripped_output.split()[1] status = OK message = "Current " + self.instance_type + " " + self.os_platform + " spot price is " + price if float(price) > float(self.critical): status = CRITICAL else: if float(price) > float(self.warning): status = WARNING if self.verbosity >= 1: return(status, "Got data from ec2-describe-spot-price-history: " + stripped_output) else: return (status, message) else: if len(output) == 0: return (CRITICAL, "Command failed. " \ + "There was no output from ec2-describe-spot-price-history") def vprint(self, threshold, message): """Prints a message if the first arg is numerically greater than or equal to the verbosity level""" if self.verbosity >= threshold: print "%s" % message def main(): """Parses args and calls func to check EC2 spot price""" tester = SpotPriceTester() parser = OptionParser() parser.add_option( "-w", "--warning", dest="warning", help="Set status to WARNING if the current EC2 spot price in U.S. dollars is above this value.") parser.add_option( "-c", "--critical", dest="critical", help="Set status to CRITICAL if the current EC2 spot price in U.S. dollars is above this value.") parser.add_option( "-i", "--instance-type", dest="instance_type", help="EC2 instance type API name (see http://aws.amazon.com/ec2/instance-types/)") parser.add_option( "-o", "--os-platform", dest="os_platform", help="O/S platform (allowable values are \"Windows\" or \"Linux/UNIX\": default \"Linux/UNIX\")") parser.add_option( "-e", "--ec2-home", dest="ec2_home", help="Path to EC2 command line tools (defaults to environment variable $EC2_HOME)") parser.add_option( "-C", "--ec2-cert", dest="ec2_cert", help="Path to EC2 certificate file (defaults to environment variable $EC2_CERT)") parser.add_option( "-K", "--ec2-private-key", dest="ec2_private_key", help="Path to EC2 private key file (defaults to environment variable $EC2_PRIVATE_KEY)") parser.add_option( "-j", "--java-home", dest="java_home", help="Path to Java installation (defaults to environment variable $JAVA_HOME)") parser.add_option( "-t", "--timeout", dest="timeout", help="Sets a timeout after which the the plugin will" \ + " self terminate. Defaults to %s seconds." \ % DEFAULT_TIMEOUT) parser.add_option( "-T", "--timing", action="store_true", dest="timing", help="Enable timer output") parser.add_option( "-v", "--verbose", action="count", dest="verbosity", help="Verbose mode. Good for testing plugin. By " \ + "default only one result line is printed as per" \ + " Nagios standards") parser.add_option( "-V", "--version", action = "store_true", dest = "version", help = "Print version number and exit" ) (options, args) = parser.parse_args() if args: parser.print_help() sys.exit(UNKNOWN) if options.version: print "%s %s" % (__title__, __version__) sys.exit(UNKNOWN) tester.warning = options.warning tester.critical = options.critical tester.instance_type = options.instance_type tester.os_platform = options.os_platform tester.verbosity = options.verbosity tester.ec2_home = options.ec2_home tester.ec2_cert = options.ec2_cert tester.ec2_private_key = options.ec2_private_key tester.java_home = options.java_home tester.timeout = options.timeout if options.timing: start_time = time.time() returncode, output = tester.test_spot_price() if options.timing: finish_time = time.time() total_time = finish_time - start_time output += ". Test completed in %.3f seconds" % total_time end(returncode, output) sys.exit(UNKNOWN) if __name__ == "__main__": try: main() except KeyboardInterrupt: print "Caught Control-C..." sys.exit(CRITICAL)