540 lines
19 KiB
Python
540 lines
19 KiB
Python
#!/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()
|
|
|
|
|
|
|
|
|