#!/usr/bin/env python3 # encoding: utf-8 import os import sys import smtplib from getpass import getpass import datetime import time import threading import configparser import argparse #JuMonitor : A simple monitoring script #This scripts regularly pings client machines, and can send alerts by mail. #We use threading to do two things at the same time : pinging clients, and putting back dead clients in the live list of clients to ping them again. We also have a thread for log sending. #We use SMTP with TLS here, but it's easy to use unsecure TLS : see the sendMail() function #This script is made for Linux. Please use free software and OS ! #BIEN VERIFIER QUE LE PING DES MORTS ET LES RAISE FONCTIONNENT #A faire : En plus du ping, proposer d'ouvrir des connexions. On peut peut-être faire du wget sur les pages web? #A faire : Proposer le résultat sous la forme de tableau à envoyer régulièrement. #A faire : Proposer de vider le log tous les X #A faire : Résultat graphique (webui) #TODO : SNMP? #Justine PELLETREAU ###########################################FUNCTIONS####################### ##################### def timeNow(): '''This function returns current date and time formatted as a string. no args Returns : now : a str containing "XX month YEAR 12:00:00" ''' now=datetime.datetime.now().strftime("%d %b %Y %H:%M:%S") return now def log(action): ''' This function is used for logging "action" (preceeded by datetime) into a file called ./jumonitor.log. Also prints onscreen it if verbose is set. Args: -action : an str containing the contents to log. GLOBAL VAR (optional): -verbose : a bool defining if we print to screen or not. No returns. ''' args = getArgs() #Getting the args because we want to know verbosity verbose = args.verbose with open("jumonitor.log", "a") as mylog: formatted_action="{} ====> {}".format(timeNow(), action) mylog.write(formatted_action) mylog.write("\n") if verbose: print(formatted_action) def getClients(): ''' This function gets a list client machines as a couple name - ip, and returns it in a dict. No args Returns: -clients : the dict containing our clients ''' clients = {} #Dict of all clients to ping name, ip = "", "" #Name and IP address of a client while True: #Loop ends when the function returns name = str(input("Enter the NAME of next client machine (END if you're finished) > ")) if name == 'END': return clients #End the function when receiving END ip = str(input("Enter IP Address of next client machine > ")) clients[name] = ip return clients def getMail(): ''' This function asks for info regarding email sending, stores it in a dict. No args Returns : -mail_info : a dict containing info about the mail server and the user ''' mail_info = {} mail_info['server_address'] = str(input("Enter the address of SMTP with TLS server \n(For gmail, use smtp.gmail.com) : ")) mail_info['server_port'] = int(input("Port? (587 generally works) : ")) mail_info['server_username'] = str(input("Enter the email address the emails will be sent from: ")) mail_info['server_password'] = getpass("Password ? : ") mail_info['server_recipient'] = str(input("Enter email address to send emails to (it can be the same address!): ")) return mail_info def sendMail(mail_text, mail_info): ''' This function sends an email. Args : -mail_text : the text of the mail to send -mail_info : dict generated by the getMail() function No returns ''' log("Now trying to send an email. This process is optimized for gmail.") #Giving info... server = smtplib.SMTP(mail_info['server_address'], mail_info['server_port']) #Creating the server object try: server.ehlo() # Connecting to the server server.starttls() # Using TLS for obvious safety reasons, remove for unsecure connection at your own risk server.login(mail_info['server_username'], mail_info['server_password']) # Loging into gmail server.sendmail(mail_info['server_username'], mail_info['server_recipient'], mail_text) #Sending the mail except: log("Something went wrong trying to send the mail. Please check settings and retry.\n===>You may need to allow unsecure apps to send emails in your gmail account settings.\nmailinfo are : {}".format(mailinfo)) exit() else: log("Mail successfully sent!") def pingTest(ipaddress): ''' This function pings an IP address, returns True if address answers, returns False if address does not answer. Args : -ipaddress : a str containing an IP address Returns: -True : address answered -False : address did not answer ''' cmd = os.popen("ping -c 1 {}".format(ipaddress)) cmdresult = cmd.read() if "1 received" in cmdresult: return True else: return False def makeAlert(ip, name, interval): ''' This Function generates an alert to send via email, telling that a client machine does not answer ping. args : -ip : IP address of said client -name : name of said client (FQDN or not) -interval : interval in minutes (after which we will retry pinging the client) returns: -text : A string containing the raw text of the email, to send directly via smtp ''' date_formatted = timeNow() hostname = os.popen("hostname") #Getting hostname of local machine hostname = hostname.read() hostname = hostname[:2] #Getting rid of \n generated by hostname command text = "Subject: Client {} not answering\n\nHello\n This mail informs you that at the time of {}, pinging the client {} (Name or IP address : {}) from machine {} resulted in a failure.\n We will retry pinging them in {} minutes.".format(name, date_formatted, name, ip, hostname, interval) return text def monitor(send_alerts, clients, dead_clients, interval, interval_dead_clients, mailinfo): ''' This function is used to monitor clients by pinging them regularly. It displays info in real time. Args: -send_alerts : a bool that says if the client wants alerts by mail or not -clients : dict of live clients -dead_clients : dict of dead clients -interval : an interval in seconds, the time we wait inbetween pings -interval_dead_clients : an interval in minutes, the time we wait before retrying dead clients -mailinfo : a dict containing info to send mails, generated by getMail() No returns ''' while True: for i in list(clients): if pingTest(clients[i]): log("Client {} answered at {}.".format(i, timeNow())) else: log("Client {} did not answer or is unknown.".format(i, mailinfo['server_recipient'])) if send_alerts: #Only send a mail if the user wants it sendMail(makeAlert(clients[i], i, interval_dead_clients), mailinfo) log("\tSending an alert mail to {}".format(mail_info['server_recipient'])) dead_clients[i] = clients[i] #Poor client is dead... del clients[i] time.sleep(interval) log("List of dead clients so far : {}".format(str(dead_clients))) def raiseClient(clients, dead_clients, interval, mailinfo): ''' This function waits a given time, and pings the dead clients We send a mail and put them back in the live queue if they answer. Made to use as a thread. Args: -clients : dict of live clients -dead_clients : dict of dead clients -interval : A time in minutes -mailinfo : generated by getmail() No returns ''' while True: time.sleep(interval) for i in list(dead_clients): if pingTest(dead_clients[i]): clients[i] = dead_clients[i] log(i, "is back ! Putting it back in the live queue and sending an email.") sendMail("Subject : client {} active again\n\nThis is a mail from JuMonitor to inform you that the client {} ( {} ) is live again.".format(i, i, dead_clients[i]), mailinfo) del dead_clients[i] else: log("{} still not answering".format(i)) def sendTestmail(mailinfo): ''' Sends a test e-mail. No args No returns ''' text = "Subject: Hi\n\nHi, this a test from JuMonitor. Hope you like this program :D" try: sendMail(text, mailinfo) log('Test Mail sent. Please check inbox/spam.') except exception as e: log("Sending test mail failed. sendMail returned {}".format(e)) def sendLog(log: str, mailinfo: dict, waittime :int): ''' This function is used to send the log file by mail every time minutes. Args: -log : name of log file (str) -mailinfo : dict generated by getMail() -time : an int indicating a number of minutes No returns ''' #Getting user's hostname hostname = os.popen("hostname") hostname = hostname.read() hostname = hostname[:2] #Getting rid of \n generated by hostname command while True: time.sleep(waittime*60) with open(log, "r") as logfile: sendMail("Subject: logfile from JuMonitor running on{}\n\n{}".format(hostname, logfile.read()), mailinfo) log("===>Log sent to {}".format(mailinfo['server_recipient'])) def doYouWantLog(): ''' This functions asks the client if they want to regularly receive the logfile by mail. No args -Returns: log_send_interval : interval in minutes at wich the client wants to receive the logfile. if 0, don't send the log file! ''' log_send_interval = 0 client_wants_log=False answer = str(input("Do you want to regularly receive the logfile by mail? [y/n] > ")) if answer == "y": client_wants_log = True if client_wants_log: log_send_interval = int(input("==> At wich interval (in minutes)?\n(An interval of 0 will not send anything) : ")) return log_send_interval def doYouWantAlerts(): ''' Asks the user if if he wants to receive alerts by mail. No args Returns True / False ''' answer=str(input("Do you want to be informed by mail when a client stops answering to our pings (becomes 'dead')? [y/n] > ")) if answer == "y": return True else: return False def loadConfig(): ''' Loads the configuration file jumonitor.conf and returns all the variables. No args Returns: -Clients : A dict containing all the clients and their IPs -log_send_interval = 0 or + -send_alerts = True / False -mailinfo = Dict like one generated by getMail() -interval = an int -interval_dead_clients = an int ''' #Initializing the vars with default values just in case clients={} dead_clients={} #Will always start empty log_send_interval = 0 send_alerts = False mailinfo = {} interval = 5 interval_dead_clients = 5 config=configparser.ConfigParser() #Creating parser object config.read('jumonitor.conf') #Reading the file (it is loaded in the parser itself) #Getting the clients for key, value in config.items('Clients'): clients[key]=value #Getting the Alerts log_send_intervals = int(config['Alerts']['log_send_interval']) #is an int send_alerts = config['Alerts'].getboolean('send_alerts') #Getting Mailinfo for key, value in config.items('Mailinfo'): mailinfo[key]=value #Getting intervals interval = int(config['Intervals']['interval']) interval_dead_clients = int(config['Intervals']['interval_dead_clients']) return clients, dead_clients, log_send_interval, send_alerts, mailinfo, interval, interval_dead_clients def dynamicStart(): '''Is used to ask for all info when starting in dynamic mode (ie, not loading from the config but asking for info). No args Returns: -Clients : A dict containing all the clients and their IPs -log_send_interval = 0 or + -send_alerts = True / False -mailinfo = Dict like one generated by getMail() -interval = an int -interval_dead_clients = an int ''' filename = "NOTHING" #The file where this will be saved mode = 0 print("*" * 20, "Welcome to JuMonitor", "*" * 20) print("This is a simple monitoring program, designed to quickly be informed when one of your servers goes down and does not answer.") print("As your started in dynamic mode, that means you do not have anything configured yet. We will be asking for info.") print("After that, you will have the possibility to save evrything in a configuration file.\n") print("First, we need to know who you want to monitor.") clients = getClients() dead_clients = {} #Used to store clients who don't answer #Does the user want to receive the logfile by mail? log_send_interval = doYouWantLog() #Does the client want to receive alerts by mail? send_alerts=doYouWantAlerts() #If any mails have to be sent, ask for mail info if send_alerts or log_send_interval > 0: print("Next, we need mail info to warn you about dead clients.") mailinfo = getMail() ###Checking if email sending works test_mail = "" while test_mail != "y" and test_mail != "n": test_mail = str(input("Sending a test mail is recommended. Try it? [y/n] : ")) if test_mail == "y": sendTestmail(mailinfo) else: mailinfo={} #Just to ensure no function crashes, an empty mailfo is required #Monitoring part interval = int(input("At which interval must we ping the clients (in seconds)? : ")) interval_dead_clients = int(input("At which interval should we retry to ping dead clients?\n(In seconds) : ")) #Saving/Printing the config file print("Now that we have everything we need, you might want to save it somewhere.") print("Enter a number according to the following:\n\t1 : Save to a file\n\t2 : print everything on screen\n\t3 : both ") while mode not in range(1, 4): mode = int(input(" > ")) #Calling our conf-writing function #dead_clients is not passed to it as it always starts as {} saveConfigFile(clients, log_send_interval, send_alerts, mailinfo, interval, interval_dead_clients, filename, mode) print("\nOk, configuration is over ! \nIf you saved your configuration in a file, be warned that you have to name it jumonitor.conf to get it to automatically load.\n\nStarting monitoring.") return clients, dead_clients, log_send_interval, send_alerts, mailinfo, interval, interval_dead_clients def saveConfigFile(clients, log_send_interval, send_alerts, mailinfo, interval, interval_dead_clients, filename, mode): ''' This functions takes info given to it and writes to a configuration file, or prints it on screen, or both, depending on the mode (1, 2 or 3). Args: (JuMonitor Config info) -Clients : A dict containing all the clients and their IPs -log_send_interval = 0 or + -send_alerts = True / False -mailinfo = Dict like one generated by getMail(), OR an empty dict if no mailinfo was genereated previously -interval = an int -interval_dead_clients = an int -mailinfo : a dict generated by getmail() (Function vars) -filename : the name given to the config file -mode : an int equal to 1, 2 or 3 dictating the behaviour of this function: 1 : save to a file 2 : print everything on screen 3 : both ''' #Taking into consideration that we might not want to save to a file if mode == 3: filename = "NOTHING" #Used to store the config file as a str to log it contents = "" config=configparser.ConfigParser() #We take every info and put it the config object #Clients config['DEFAULT']={} config['Clients']=clients #Alerts config['Alerts']=\ {'log_send_interval' : log_send_interval,\ 'send_alerts' : send_alerts} if mailinfo != {}: #Mail config['Mailinfo']=\ {'server_address' : mailinfo['server_address'],\ 'server_port' : mailinfo['server_port'],\ 'server_username' : mailinfo['server_username'],\ 'server_password' : mailinfo['server_password'],\ 'server_recipient' : mailinfo['server_recipient']} else: config['Mailinfo']={} #Intervals config['Intervals']=\ {'interval': interval,\ 'interval_dead_clients' : interval_dead_clients} #Putting everything into a str for logging for section in config.sections(): contents += str(dict(config[section])) #We now print / write the file if mode == 1 or mode == 3: filename = str(input("What name do you want to give to your file? Watch out, any existing file will be overwritten. \nYou can include the extension, like .conf.\n Name it jumonitor.conf if you want it to automatically load. > ")) with open(filename, 'w') as configfile: config.write(configfile) if mode == 2 or mode == 3: print(contents) #Logging log("Config Info given : {}".format(contents)) def getArgs(): '''Gets all the arguments passed to the script and returns them in a parse_args()-type object. No args Returns: -args : an args object containing all the optional arguments passed to the script. ''' parser = argparse.ArgumentParser() parser.add_argument("-d", "--dynamic", help="Starts in dynamic mode.", action="store_true") parser.add_argument("-t", "--testmail", help="Sends a test e-mail.", action="store_true") parser.add_argument("-v", "--verbose", help="Print all debug info to screen as well as to logfile", action="store_true") #Creating the args object args=parser.parse_args() return args #########################################################MAIN PROGRAM############################ #Getting the args... args=getArgs() #Tell that we are starting in a clear way log("#\n#\nSTARTING at {}".format(timeNow())) #Is the launch dynamic or static? if args.dynamic: log("Getting info dynamically as -d has been used. ") clients, dead_clients, log_send_interval, send_alerts, mailinfo, interval, interval_dead_clients = dynamicStart() else: log("Configuration information loading from jumonitor.conf") clients, dead_clients, log_send_interval, send_alerts, mailinfo, interval, interval_dead_clients=loadConfig() #Send a test mail? if args.testmail: if mailinfo != {}: sendTestmail() else: log("Could not send test mail ! mailinfo are empty") ###THREADS threadMonitor = threading.Thread(target=monitor, args=(send_alerts, clients, dead_clients, interval, interval_dead_clients, mailinfo)) threadRaise = threading.Thread(target=raiseClient, args=(clients, dead_clients, interval_dead_clients, mailinfo)) threadLogSend = threading.Thread(target=sendLog, args=("jumonitor.log", mailinfo, log_send_interval)) #These threads must run together, no RLocks here ! threadMonitor.start() threadRaise.start() #Only send logs if the client wants it if log_send_interval > 0: threadLogSend.start()