cleaned linspector ,refactored command-line parsing, and fixed the Logger
This commit is contained in:
parent
c608131174
commit
5b09f02d68
8 changed files with 445 additions and 213 deletions
|
|
@ -9,7 +9,7 @@ from hostgroups import parseHostGroupList
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
def __init__(self, configFile):
|
def __init__(self, configFile, log):
|
||||||
self.configfile = configFile
|
self.configfile = configFile
|
||||||
f = open(configFile)
|
f = open(configFile)
|
||||||
self.config = f.read()
|
self.config = f.read()
|
||||||
|
|
@ -21,16 +21,16 @@ class Config:
|
||||||
|
|
||||||
self.filters = parseFilterList(self.dict['filters'])
|
self.filters = parseFilterList(self.dict['filters'])
|
||||||
|
|
||||||
self.members = parseMemberList(self.dict['members'], self.filters)
|
self.members = parseMemberList(self.dict['members'], self.filters, log)
|
||||||
|
|
||||||
self.periods = parsePeriodList(self.dict['periods'])
|
self.periods = parsePeriodList(self.dict['periods'])
|
||||||
|
|
||||||
self.hosts = parseHostList(self.dict['hosts'], self.services)
|
self.hosts = parseHostList(self.dict['hosts'], self.services, log)
|
||||||
|
|
||||||
self.hostgroups = parseHostGroupList(self.dict['hostgroups'],
|
self.hostgroups = parseHostGroupList(self.dict['hostgroups'],
|
||||||
self.hosts,
|
self.hosts,
|
||||||
self.members,
|
self.members,
|
||||||
self.periods,
|
self.periods,
|
||||||
self.services)
|
self.services, log)
|
||||||
|
|
||||||
#self.layouts = LayoutList(self.dict['layouts'], self.hostgroups)
|
#self.layouts = LayoutList(self.dict['layouts'], self.hostgroups)
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ class HostGroupService:
|
||||||
return "HostgroupService { " + str(self.service) + ", " + str(self.periods) + "}"
|
return "HostgroupService { " + str(self.service) + ", " + str(self.periods) + "}"
|
||||||
|
|
||||||
|
|
||||||
def parseHostGroupList(hostgroups, hosts, members, periods, services):
|
def parseHostGroupList(hostgroups, hosts, members, periods, services, log):
|
||||||
parsedHostGroups = []
|
parsedHostGroups = []
|
||||||
for hgname, hgValues in hostgroups.items():
|
for hgname, hgValues in hostgroups.items():
|
||||||
hostGroup = HostGroup(hgname)
|
hostGroup = HostGroup(hgname)
|
||||||
|
|
@ -47,8 +47,8 @@ def parseHostGroupList(hostgroups, hosts, members, periods, services):
|
||||||
hostGroup.services = []
|
hostGroup.services = []
|
||||||
for serviceName, servicePeriods in hgValues['services'].items():
|
for serviceName, servicePeriods in hgValues['services'].items():
|
||||||
service = filter(lambda s: s.name in serviceName, services)
|
service = filter(lambda s: s.name in serviceName, services)
|
||||||
if not service:
|
if len(service) == 0:
|
||||||
print "warning: Service " + serviceName + " is not defined for Hostgroup " + hgname
|
log.w("Service " + serviceName + " is not defined for Hostgroup " + hgname)
|
||||||
continue
|
continue
|
||||||
service = service[0]
|
service = service[0]
|
||||||
hostGroupPeriods = filter(lambda p: p.name in servicePeriods, periods)
|
hostGroupPeriods = filter(lambda p: p.name in servicePeriods, periods)
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ class HostService:
|
||||||
return ret;
|
return ret;
|
||||||
|
|
||||||
|
|
||||||
def parseHostList(hosts, services):
|
def parseHostList(hosts, services, log):
|
||||||
'''parse the HostList and replace any command as nessesary '''
|
'''parse the HostList and replace any command as nessesary '''
|
||||||
#precompiled regexPattern which finds replacements in service strings
|
#precompiled regexPattern which finds replacements in service strings
|
||||||
pattern = re.compile("@(\w+)")
|
pattern = re.compile("@(\w+)")
|
||||||
|
|
@ -79,7 +79,7 @@ def parseHostList(hosts, services):
|
||||||
#any remainig parm should be a replacement
|
#any remainig parm should be a replacement
|
||||||
for parm in params:
|
for parm in params:
|
||||||
if parm not in replacements:
|
if parm not in replacements:
|
||||||
print "warning: undefined parameter: " + parm + " in host " + host.name + " from service " + service.name
|
log.w("undefined parameter: " + parm + " in host " + host.name + " from service " + service.name)
|
||||||
continue
|
continue
|
||||||
#replace our ServiceCommand with the parameter_value (search, replacement, string)
|
#replace our ServiceCommand with the parameter_value (search, replacement, string)
|
||||||
hostService.setCommand(re.sub('@' + parm, params[parm], hostService.getCommand()))
|
hostService.setCommand(re.sub('@' + parm, params[parm], hostService.getCommand()))
|
||||||
|
|
@ -91,14 +91,14 @@ def parseHostList(hosts, services):
|
||||||
#replacements should be empty now.
|
#replacements should be empty now.
|
||||||
#If not we cannot use this command as some values are missing
|
#If not we cannot use this command as some values are missing
|
||||||
if replacements:
|
if replacements:
|
||||||
print "warning: Hostservice " + servicename + " from host " + host.name + " is ignored because of missing replacements: " + str(
|
log.w("Hostservice " + servicename + " from host " + host.name + " is ignored because of missing replacements: " + str(
|
||||||
replacements)
|
replacements))
|
||||||
else:
|
else:
|
||||||
#anything ok! add to our valid hostServices
|
#anything ok! add to our valid hostServices
|
||||||
hostServices.append(hostService)
|
hostServices.append(hostService)
|
||||||
#we could't find the service defined in this host. Service ignored!
|
#we could't find the service defined in this host. Service ignored!
|
||||||
if not found:
|
if not found:
|
||||||
print "warning: Service " + servicename + " not defined in host " + host.name
|
log.w("Service " + servicename + " not defined in host " + host.name)
|
||||||
#replace host.service member by parsed HostService Objects
|
#replace host.service member by parsed HostService Objects
|
||||||
host.services = hostServices
|
host.services = hostServices
|
||||||
return parsedHosts
|
return parsedHosts
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ class MemberFilter:
|
||||||
return "Filter:" + str(self.filter) + " Value:" + self.value
|
return "Filter:" + str(self.filter) + " Value:" + self.value
|
||||||
|
|
||||||
|
|
||||||
def parseMemberList(members, filters):
|
def parseMemberList(members, filters, log):
|
||||||
parsedMembers = [Member(nameid, **values) for nameid, values in members.items()]
|
parsedMembers = [Member(nameid, **values) for nameid, values in members.items()]
|
||||||
for member in parsedMembers:
|
for member in parsedMembers:
|
||||||
mFilter = []
|
mFilter = []
|
||||||
|
|
@ -39,6 +39,6 @@ def parseMemberList(members, filters):
|
||||||
memberFilter.command = re.sub('@member', replacement, filt.command)
|
memberFilter.command = re.sub('@member', replacement, filt.command)
|
||||||
mFilter.append(memberFilter)
|
mFilter.append(memberFilter)
|
||||||
if not found:
|
if not found:
|
||||||
print "warning: filter: " + filtername + " is not defined in member " + member.name
|
log.w("filter: " + filtername + " is not defined in member " + member.name)
|
||||||
member.filters = mFilter
|
member.filters = mFilter
|
||||||
return parsedMembers
|
return parsedMembers
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,62 @@
|
||||||
from datetime import datetime
|
import logging
|
||||||
|
import logging.handlers
|
||||||
DEBUG = "[debug]"
|
import os
|
||||||
NOTICE = "[notice]"
|
import os.path as path
|
||||||
WARNING = "[warning]"
|
|
||||||
|
|
||||||
|
|
||||||
def logVerbose(message, verbose=True):
|
class Logger():
|
||||||
if verbose:
|
"""Logger class that prints its messages and keeps them also inside a logfile"""
|
||||||
print DEBUG + " " + str(message)
|
|
||||||
|
def __init__(self, logfile="./linspector.log", logLevel=logging.DEBUG, logfileLevel=logging.DEBUG):
|
||||||
|
"""
|
||||||
|
initializes a new Logger object.
|
||||||
|
|
||||||
|
params:
|
||||||
|
logLevel the LoggingLevel from the console output (DEBUG default)
|
||||||
|
logfile the file where to log. Logs are rotated by default.
|
||||||
|
logfileLevel the LoggingLevel for the file Logger. (DEBUG default)
|
||||||
|
"""
|
||||||
|
|
||||||
|
logfile = path.expanduser(logfile)
|
||||||
|
if not path.exists(path.dirname(logfile)):
|
||||||
|
os.makedirs(path.dirname(logfile))
|
||||||
|
|
||||||
|
self.log = logging.getLogger("LinspectorLogger")
|
||||||
|
self.log.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
consoleHandler = logging.StreamHandler()
|
||||||
|
consoleHandler.setLevel(logLevel)
|
||||||
|
|
||||||
|
fileHandler = logging.handlers.RotatingFileHandler(logfile, maxBytes=1024000, backupCount=4)
|
||||||
|
fileHandler.setLevel(logfileLevel)
|
||||||
|
|
||||||
|
consoleFormatter = logging.Formatter('[%(levelname)s]: %(message)s')
|
||||||
|
fileFormatter = logging.Formatter('%(asctime)s [%(levelname)s]: %(message)s')
|
||||||
|
|
||||||
|
consoleHandler.setFormatter(consoleFormatter)
|
||||||
|
fileHandler.setFormatter(fileFormatter)
|
||||||
|
|
||||||
|
self.log.addHandler(consoleHandler)
|
||||||
|
self.log.addHandler(fileHandler)
|
||||||
|
|
||||||
|
def d(self, message):
|
||||||
|
self.log.debug(message)
|
||||||
|
|
||||||
|
def i(self, message):
|
||||||
|
self.log.info(message)
|
||||||
|
|
||||||
|
def w(self, message):
|
||||||
|
self.log.warn(message)
|
||||||
|
|
||||||
|
def e(self, message):
|
||||||
|
self.log.error(message)
|
||||||
|
|
||||||
|
def c(self, message):
|
||||||
|
self.log.critical(message)
|
||||||
|
self.log.critical(message)
|
||||||
|
|
||||||
|
|
||||||
def logNotice(message, verbose=True):
|
def close(self):
|
||||||
if verbose:
|
logging.shutdown()
|
||||||
print NOTICE + " " + str(message)
|
|
||||||
|
|
||||||
|
|
||||||
def logWarning(message):
|
|
||||||
print WARNING + " " + message
|
|
||||||
|
|
||||||
|
|
||||||
def logWarningConfig(thefile="file", missing="missing"):
|
|
||||||
logWarning("in " + thefile + ": The " + missing + " is not defined")
|
|
||||||
|
|
||||||
|
|
||||||
def writeLogToFile(logfile, message):
|
|
||||||
f = open(logfile, 'a')
|
|
||||||
f.write("[" + str(datetime.now()) + "] " + message + '\n')
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
|
|
||||||
class Logger:
|
|
||||||
def __init__(self, logfile="/dev/null"):
|
|
||||||
self.logfile = logfile
|
|
||||||
|
|
||||||
def logSomething(self, message, verbose=False):
|
|
||||||
f = open(self.logfile, 'a')
|
|
||||||
f.write("[" + str(datetime.now()) + "] " + message + '\n')
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
|
|
|
||||||
205
linspector
205
linspector
|
|
@ -1,183 +1,74 @@
|
||||||
#!/usr/bin/python2.7 -tt
|
#!/usr/bin/python2.7 -tt
|
||||||
|
|
||||||
import sys
|
VERSION = "0.1.1/TETRIS"
|
||||||
import time
|
import argparse
|
||||||
import getopt
|
from lib.core.logger import Logger
|
||||||
from lib.core.daemon import Daemon
|
|
||||||
from lib.config.config import Config
|
from lib.config.config import Config
|
||||||
from lib.core import logger
|
|
||||||
from apscheduler.scheduler import Scheduler
|
from apscheduler.scheduler import Scheduler
|
||||||
import pprint
|
import logging
|
||||||
|
import subprocess as sp
|
||||||
NAME = "linspector"
|
|
||||||
VERSION = "0.1/TETRIS"
|
|
||||||
|
|
||||||
_configfile = str(sys.path[0]) + "/linspector.json"
|
|
||||||
_logfile = str(sys.path[0]) + "/linspector.log"
|
|
||||||
_pidfile = "/tmp/linspector.pid"
|
|
||||||
|
|
||||||
|
|
||||||
class LinspectorDaemon(Daemon):
|
|
||||||
def run(self):
|
|
||||||
"""
|
|
||||||
parse the joblist here and add each job to cron.
|
|
||||||
"""
|
|
||||||
sched = Scheduler()
|
|
||||||
sched.start()
|
|
||||||
|
|
||||||
x = sched.add_cron_job(job_function, second='*/1', args=['1!'])
|
def parseArgs():
|
||||||
if x is not None:
|
parser = argparse.ArgumentParser(
|
||||||
logger.writeLogToFile(_logfile, str(x.__dict__))
|
description="Linspector is for monitoring the vital information of hosts, services and devices in a network.",
|
||||||
else:
|
epilog="linspector is not some program expecting computers to run!",
|
||||||
logger.writeLogToFile(_logfile, "sdfasdfsdf")
|
prog="linspector")
|
||||||
|
|
||||||
sched.add_cron_job(job_function, second='*/2', args=['2!'])
|
parser.add_argument("action", choices=["start", "stop", "restart", "attach"],
|
||||||
sched.add_cron_job(job_function, second='*/4', args=['4!'])
|
help="defines if linspector should beeing attached, started, stopped or restarted.")
|
||||||
sched.add_cron_job(job_function, second='*/5', args=['5!'])
|
parser.add_argument("--version", action="version", version="%(prog)s "+str(VERSION))
|
||||||
sched.add_cron_job(job_function, second='*/8', args=['8!'])
|
parser.add_argument("-c", "--config", default="./linspector.json",
|
||||||
sched.add_cron_job(job_function, second='*/10', args=['10!'])
|
help="select configfile to use")
|
||||||
sched.add_cron_job(job_function, second='*/20', args=['20!'])
|
parser.add_argument("-l", "--logfile", default="./log/linspector.log", metavar="FILE",
|
||||||
sched.add_cron_job(job_function, second='*/40', args=['40!'])
|
help="set logfile to use")
|
||||||
sched.add_cron_job(job_function, second='*', args=['0!'])
|
parser.add_argument("-p", "--pidfile", default="./tmp/linspector.pid", metavar="FILE",
|
||||||
|
help="set the pidfile to use (default: /tmp/linspector.pid)")
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
logger.writeLogToFile(_logfile, "Running!")
|
|
||||||
except Exception as err:
|
|
||||||
logger.writeLogToFile(_logfile, str(err))
|
|
||||||
sys.exit(1)
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
|
|
||||||
def job_function(mes):
|
output=parser.add_mutually_exclusive_group()
|
||||||
logger.writeLogToFile(_logfile, "Function: job_function says " + str(mes) + ", from cron.")
|
output.add_argument("-q","--quiet", action="store_const", dest="loglevel", const=logging.ERROR,
|
||||||
|
help="output only errors")
|
||||||
|
output.add_argument("-w", "--warning", action="store_const", dest="loglevel", const=logging.WARNING,
|
||||||
def usage():
|
help="output warnings")
|
||||||
print "usage: linspector [-cdhlprsSvV]"
|
output.add_argument("-v","--verbose", action="store_const", dest="loglevel", const=logging.INFO,
|
||||||
print "-c, --config=FILE select configfile to use"
|
help="output infos")
|
||||||
print "-d, --daemonize daemonize process"
|
output.add_argument("-d", "--debug", action="store_const", dest="loglevel", const=logging.DEBUG,
|
||||||
print "-h, --help this help"
|
help="output debug infos")
|
||||||
print "-l, --logfile=FILE set logfile to use"
|
output.set_defaults(loglevel=logging.INFO)
|
||||||
print "-p, --pidfile=FILE set the pidfile to use (default: /tmp/linspector.pid)"
|
return parser.parse_args()
|
||||||
print "-r, --restart restart the daemon"
|
|
||||||
print "-s, --start start the daemon"
|
|
||||||
print "-S, --stop stop the daemon"
|
|
||||||
print "-v, --verbose verbose mode"
|
|
||||||
print "-V, --version show version information"
|
|
||||||
|
|
||||||
|
|
||||||
def version():
|
|
||||||
print NAME + " " + VERSION
|
|
||||||
print "copyright (c) 2011-2013 by Johannes Findeisen and Rafael Timmerberg"
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
try:
|
args=parseArgs()
|
||||||
opts, args = getopt.getopt(sys.argv[1:],
|
log=Logger(args.logfile , args.loglevel)
|
||||||
"hl:c:p:vVrsS",
|
|
||||||
["help", "logfile=",
|
|
||||||
"config=", "pidfile=",
|
|
||||||
"verbose", "version",
|
|
||||||
"restart", "start", "stop"])
|
|
||||||
except getopt.GetoptError, err:
|
|
||||||
print str(err)
|
|
||||||
usage()
|
|
||||||
sys.exit(2)
|
|
||||||
verbose = False
|
|
||||||
restart = False
|
|
||||||
start = False
|
|
||||||
stop = False
|
|
||||||
for o, a in opts:
|
|
||||||
if o in ("-v", "--verbose"):
|
|
||||||
verbose = True
|
|
||||||
elif o in ("-V", "--version"):
|
|
||||||
version()
|
|
||||||
sys.exit()
|
|
||||||
elif o in ("-h", "--help"):
|
|
||||||
usage()
|
|
||||||
sys.exit()
|
|
||||||
elif o in ("-c", "--config"):
|
|
||||||
configfile = a
|
|
||||||
global _configfile
|
|
||||||
_configfile = a
|
|
||||||
elif o in ("-l", "--logfile"):
|
|
||||||
logfile = a
|
|
||||||
global _logfile
|
|
||||||
_logfile = a
|
|
||||||
elif o in ("-p", "--pidfile"):
|
|
||||||
pidfile = a
|
|
||||||
global _pidfile
|
|
||||||
_pidfile = a
|
|
||||||
elif o in ("-r", "--restart"):
|
|
||||||
restart = True
|
|
||||||
elif o in ("-s", "--start"):
|
|
||||||
start = True
|
|
||||||
elif o in ("-S", "--stop"):
|
|
||||||
stop = True
|
|
||||||
|
|
||||||
if start is True or restart is True:
|
log.i("parsed arguments")
|
||||||
config = Config(_configfile)
|
|
||||||
#pprint.pprint(str(config.layouts))
|
|
||||||
#pprint.pprint(str(config.dict["hostgroups"]))
|
|
||||||
#pprint.pprint(str(config.filters.))
|
|
||||||
|
|
||||||
"""
|
if args.action=="start":
|
||||||
for service in config.services:
|
log.i("starting linspector: reading config...")
|
||||||
pprint.pprint(str(service))
|
config=Config(args.config, log)
|
||||||
|
log.d("parsed config: " + str(config))
|
||||||
|
for hg in config.hostgroups:
|
||||||
|
log.i(str(hg))
|
||||||
|
elif args.action=="stop":
|
||||||
|
log.i("stopping linspector is currently unsupported")
|
||||||
|
elif args.action=="restart":
|
||||||
|
sp.call("./linspector stop")
|
||||||
|
sp.call("./linspector start --config " + args.config + " --logfile " + args.logfile + " --pidfile " + args.pidfile)
|
||||||
|
elif args.action=="attach":
|
||||||
|
log.i("attaching linspector is currently unsupported")
|
||||||
|
|
||||||
for filter in config.filters:
|
|
||||||
pprint.pprint(str(filter))
|
|
||||||
|
|
||||||
for member in config.members:
|
|
||||||
pprint.pprint(str(member))
|
|
||||||
|
|
||||||
for period in config.periods:
|
|
||||||
pprint.pprint(str(period))
|
|
||||||
|
|
||||||
for host in config.hosts:
|
|
||||||
pprint.pprint(str(host))
|
|
||||||
"""
|
|
||||||
|
|
||||||
for hostgroup in config.hostgroups:
|
log.close()
|
||||||
pprint.pprint(str(hostgroup))
|
|
||||||
|
|
||||||
#for layout in config.layouts.layouts:
|
|
||||||
# pprint.pprint(str(layout
|
|
||||||
|
|
||||||
#logger.writeLogToFile(_logfile, "config.layouts.lyouts: " + str([str(i) for i in config.layouts.layouts]))
|
|
||||||
#logger.writeLogToFile(_logfile, "config.hostgroups: " + config.hostgroups)
|
|
||||||
#logger.writeLogToFile(_logfile, "config.hosts: " + str([str(i) for i in config.hosts]))
|
|
||||||
#logger.writeLogToFile(_logfile, "config.services: " + str([str(i) for i in config.services]))
|
|
||||||
#logger.writeLogToFile(_logfile, "config.members: " + str([str(i) for i in config.members]))
|
|
||||||
#logger.writeLogToFile(_logfile, "config.periods: " + str([str(i) for i in config.periods]))
|
|
||||||
#logger.writeLogToFile(_logfile, "config.filters: " + str([str(i) for i in config.filters]))
|
|
||||||
#logger.writeLogToFile(_logfile, "X:" + config.periods[0])
|
|
||||||
|
|
||||||
"""
|
|
||||||
linspector = LinspectorDaemon(_pidfile)
|
|
||||||
|
|
||||||
if start is True:
|
|
||||||
logger.writeLogToFile(_logfile, "Starting Linspector...")
|
|
||||||
logger.writeLogToFile(_logfile, "Path: " + str(sys.path[0]))
|
|
||||||
logger.writeLogToFile(_logfile, "Configfile: " + _configfile)
|
|
||||||
logger.writeLogToFile(_logfile, "Logfile: " + _logfile)
|
|
||||||
logger.writeLogToFile(_logfile, "Pidfile: " + _pidfile)
|
|
||||||
linspector.start()
|
|
||||||
elif stop is True:
|
|
||||||
logger.writeLogToFile(_logfile, "Stopping Linspector...")
|
|
||||||
logger.writeLogToFile(_logfile, "Pidfile: " + _pidfile)
|
|
||||||
linspector.stop()
|
|
||||||
logger.writeLogToFile(_logfile, "Terminated!")
|
|
||||||
elif restart is True:
|
|
||||||
logger.writeLogToFile(_logfile, "Restarting Linspector...")
|
|
||||||
logger.writeLogToFile(_logfile, "Path: " + str(sys.path[0]))
|
|
||||||
logger.writeLogToFile(_logfile, "Configfile: " + _configfile)
|
|
||||||
logger.writeLogToFile(_logfile, "Logfile: " + _logfile)
|
|
||||||
logger.writeLogToFile(_logfile, "Pidfile: " + _pidfile)
|
|
||||||
linspector.restart()
|
|
||||||
sys.exit(0)
|
|
||||||
"""
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
|
||||||
183
linspector.old
Executable file
183
linspector.old
Executable file
|
|
@ -0,0 +1,183 @@
|
||||||
|
#!/usr/bin/python2.7 -tt
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import getopt
|
||||||
|
from lib.core.daemon import Daemon
|
||||||
|
from lib.config.config import Config
|
||||||
|
from lib.core import logger
|
||||||
|
from apscheduler.scheduler import Scheduler
|
||||||
|
import pprint
|
||||||
|
|
||||||
|
NAME = "linspector"
|
||||||
|
VERSION = "0.1/TETRIS"
|
||||||
|
|
||||||
|
_configfile = str(sys.path[0]) + "/linspector.json"
|
||||||
|
_logfile = str(sys.path[0]) + "/linspector.log"
|
||||||
|
_pidfile = "/tmp/linspector.pid"
|
||||||
|
|
||||||
|
|
||||||
|
class LinspectorDaemon(Daemon):
|
||||||
|
def run(self):
|
||||||
|
"""
|
||||||
|
parse the joblist here and add each job to cron.
|
||||||
|
"""
|
||||||
|
sched = Scheduler()
|
||||||
|
sched.start()
|
||||||
|
|
||||||
|
x = sched.add_cron_job(job_function, second='*/1', args=['1!'])
|
||||||
|
if x is not None:
|
||||||
|
logger.writeLogToFile(_logfile, str(x.__dict__))
|
||||||
|
else:
|
||||||
|
logger.writeLogToFile(_logfile, "sdfasdfsdf")
|
||||||
|
|
||||||
|
sched.add_cron_job(job_function, second='*/2', args=['2!'])
|
||||||
|
sched.add_cron_job(job_function, second='*/4', args=['4!'])
|
||||||
|
sched.add_cron_job(job_function, second='*/5', args=['5!'])
|
||||||
|
sched.add_cron_job(job_function, second='*/8', args=['8!'])
|
||||||
|
sched.add_cron_job(job_function, second='*/10', args=['10!'])
|
||||||
|
sched.add_cron_job(job_function, second='*/20', args=['20!'])
|
||||||
|
sched.add_cron_job(job_function, second='*/40', args=['40!'])
|
||||||
|
sched.add_cron_job(job_function, second='*', args=['0!'])
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
logger.writeLogToFile(_logfile, "Running!")
|
||||||
|
except Exception as err:
|
||||||
|
logger.writeLogToFile(_logfile, str(err))
|
||||||
|
sys.exit(1)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
def job_function(mes):
|
||||||
|
logger.writeLogToFile(_logfile, "Function: job_function says " + str(mes) + ", from cron.")
|
||||||
|
|
||||||
|
|
||||||
|
def usage():
|
||||||
|
print "usage: linspector [-cdhlprsSvV]"
|
||||||
|
print "-c, --config=FILE select configfile to use"
|
||||||
|
print "-d, --daemonize daemonize process"
|
||||||
|
print "-h, --help this help"
|
||||||
|
print "-l, --logfile=FILE set logfile to use"
|
||||||
|
print "-p, --pidfile=FILE set the pidfile to use (default: /tmp/linspector.pid)"
|
||||||
|
print "-r, --restart restart the daemon"
|
||||||
|
print "-s, --start start the daemon"
|
||||||
|
print "-S, --stop stop the daemon"
|
||||||
|
print "-v, --verbose verbose mode"
|
||||||
|
print "-V, --version show version information"
|
||||||
|
|
||||||
|
|
||||||
|
def version():
|
||||||
|
print NAME + " " + VERSION
|
||||||
|
print "copyright (c) 2011-2013 by Johannes Findeisen and Rafael Timmerberg"
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
opts, args = getopt.getopt(sys.argv[1:],
|
||||||
|
"hl:c:p:vVrsS",
|
||||||
|
["help", "logfile=",
|
||||||
|
"config=", "pidfile=",
|
||||||
|
"verbose", "version",
|
||||||
|
"restart", "start", "stop"])
|
||||||
|
except getopt.GetoptError, err:
|
||||||
|
print str(err)
|
||||||
|
usage()
|
||||||
|
sys.exit(2)
|
||||||
|
verbose = False
|
||||||
|
restart = False
|
||||||
|
start = False
|
||||||
|
stop = False
|
||||||
|
for o, a in opts:
|
||||||
|
if o in ("-v", "--verbose"):
|
||||||
|
verbose = True
|
||||||
|
elif o in ("-V", "--version"):
|
||||||
|
version()
|
||||||
|
sys.exit()
|
||||||
|
elif o in ("-h", "--help"):
|
||||||
|
usage()
|
||||||
|
sys.exit()
|
||||||
|
elif o in ("-c", "--config"):
|
||||||
|
configfile = a
|
||||||
|
global _configfile
|
||||||
|
_configfile = a
|
||||||
|
elif o in ("-l", "--logfile"):
|
||||||
|
logfile = a
|
||||||
|
global _logfile
|
||||||
|
_logfile = a
|
||||||
|
elif o in ("-p", "--pidfile"):
|
||||||
|
pidfile = a
|
||||||
|
global _pidfile
|
||||||
|
_pidfile = a
|
||||||
|
elif o in ("-r", "--restart"):
|
||||||
|
restart = True
|
||||||
|
elif o in ("-s", "--start"):
|
||||||
|
start = True
|
||||||
|
elif o in ("-S", "--stop"):
|
||||||
|
stop = True
|
||||||
|
|
||||||
|
if start is True or restart is True:
|
||||||
|
config = Config(_configfile)
|
||||||
|
#pprint.pprint(str(config.layouts))
|
||||||
|
#pprint.pprint(str(config.dict["hostgroups"]))
|
||||||
|
#pprint.pprint(str(config.filters.))
|
||||||
|
|
||||||
|
"""
|
||||||
|
for service in config.services:
|
||||||
|
pprint.pprint(str(service))
|
||||||
|
|
||||||
|
for filter in config.filters:
|
||||||
|
pprint.pprint(str(filter))
|
||||||
|
|
||||||
|
for member in config.members:
|
||||||
|
pprint.pprint(str(member))
|
||||||
|
|
||||||
|
for period in config.periods:
|
||||||
|
pprint.pprint(str(period))
|
||||||
|
|
||||||
|
for host in config.hosts:
|
||||||
|
pprint.pprint(str(host))
|
||||||
|
"""
|
||||||
|
|
||||||
|
for hostgroup in config.hostgroups:
|
||||||
|
pprint.pprint(str(hostgroup))
|
||||||
|
|
||||||
|
#for layout in config.layouts.layouts:
|
||||||
|
# pprint.pprint(str(layout
|
||||||
|
|
||||||
|
#logger.writeLogToFile(_logfile, "config.layouts.lyouts: " + str([str(i) for i in config.layouts.layouts]))
|
||||||
|
#logger.writeLogToFile(_logfile, "config.hostgroups: " + config.hostgroups)
|
||||||
|
#logger.writeLogToFile(_logfile, "config.hosts: " + str([str(i) for i in config.hosts]))
|
||||||
|
#logger.writeLogToFile(_logfile, "config.services: " + str([str(i) for i in config.services]))
|
||||||
|
#logger.writeLogToFile(_logfile, "config.members: " + str([str(i) for i in config.members]))
|
||||||
|
#logger.writeLogToFile(_logfile, "config.periods: " + str([str(i) for i in config.periods]))
|
||||||
|
#logger.writeLogToFile(_logfile, "config.filters: " + str([str(i) for i in config.filters]))
|
||||||
|
#logger.writeLogToFile(_logfile, "X:" + config.periods[0])
|
||||||
|
|
||||||
|
"""
|
||||||
|
linspector = LinspectorDaemon(_pidfile)
|
||||||
|
|
||||||
|
if start is True:
|
||||||
|
logger.writeLogToFile(_logfile, "Starting Linspector...")
|
||||||
|
logger.writeLogToFile(_logfile, "Path: " + str(sys.path[0]))
|
||||||
|
logger.writeLogToFile(_logfile, "Configfile: " + _configfile)
|
||||||
|
logger.writeLogToFile(_logfile, "Logfile: " + _logfile)
|
||||||
|
logger.writeLogToFile(_logfile, "Pidfile: " + _pidfile)
|
||||||
|
linspector.start()
|
||||||
|
elif stop is True:
|
||||||
|
logger.writeLogToFile(_logfile, "Stopping Linspector...")
|
||||||
|
logger.writeLogToFile(_logfile, "Pidfile: " + _pidfile)
|
||||||
|
linspector.stop()
|
||||||
|
logger.writeLogToFile(_logfile, "Terminated!")
|
||||||
|
elif restart is True:
|
||||||
|
logger.writeLogToFile(_logfile, "Restarting Linspector...")
|
||||||
|
logger.writeLogToFile(_logfile, "Path: " + str(sys.path[0]))
|
||||||
|
logger.writeLogToFile(_logfile, "Configfile: " + _configfile)
|
||||||
|
logger.writeLogToFile(_logfile, "Logfile: " + _logfile)
|
||||||
|
logger.writeLogToFile(_logfile, "Pidfile: " + _pidfile)
|
||||||
|
linspector.restart()
|
||||||
|
sys.exit(0)
|
||||||
|
"""
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
136
log/linspector.log
Normal file
136
log/linspector.log
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
2013-05-24 02:29:22,700 [INFO]: parsed arguments
|
||||||
|
2013-05-24 02:29:22,701 [INFO]: reading config...
|
||||||
|
2013-05-24 02:29:22,703 [DEBUG]: parsed config: <lib.config.config.Config instance at 0x2365a28>
|
||||||
|
2013-05-24 02:29:22,703 [INFO]: HostGroup: all threshold: 10 parent: network
|
||||||
|
members: {
|
||||||
|
Member Id: admin Name: Peter Hansen Filters: Filter('Name: sms', 'Command: /usr/bin/warn_the_admin_sms +23345567 @+message', 'Priority: 0')Filter('Name: email', 'Command: /usr/bin/warn_the_admin_mail admin@systemchaos.org @+message', 'Priority: 1')
|
||||||
|
}
|
||||||
|
hosts: {
|
||||||
|
Host('Name: hanez', 'access: www.hanez.org', 'HostServices: {Service('Name: load', 'Command: ssh www.hanez.org uptime, 'Parser: ')warning: 8.0critical: 12.0
|
||||||
|
Service('Name: processcountbyname', 'Command: ssh www.hanez.org ps ax | grep java | wc -l, 'Parser: ')warning: 100critical: 200
|
||||||
|
Service('Name: htmlcontent', 'Command: wget -qO- @host/test.php, 'Parser: ')
|
||||||
|
Service('Name: discusage', 'Command: ssh www.hanez.org df -a /dev/sda1, 'Parser: ')warning: 80%critical: 90%
|
||||||
|
Service('Name: discusage', 'Command: ssh www.hanez.org df -a /dev/sda2, 'Parser: ')warning: 70%critical: 90%
|
||||||
|
Service('Name: discusage', 'Command: ssh www.hanez.org df -a /dev/sda3, 'Parser: ')warning: 70GBcritical: 90GB
|
||||||
|
Service('Name: processcount', 'Command: ssh www.hanez.org ps ax | wc -l, 'Parser: ')warning: 3000critical: 3800
|
||||||
|
Service('Name: fileage', 'Command: ssh www.hanez.org ls -l /var/backup/server_1, 'Parser: ')warning: 2critical: 3
|
||||||
|
Service('Name: dirsize', 'Command: ssh www.hanez.org du -chs /var/log, 'Parser: ')warning: 30000000critical: 40000000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
services: {
|
||||||
|
HostgroupService { Service('Name: load', 'Command: ssh @host uptime, 'Parser: '), [<lib.config.periods.Period instance at 0x23bc1b8>]}
|
||||||
|
HostgroupService { Service('Name: discusage', 'Command: ssh @host df -a @device, 'Parser: df'), [<lib.config.periods.Period instance at 0x23bc1b8>]}
|
||||||
|
HostgroupService { Service('Name: ping', 'Command: ping @host, 'Parser: '), [<lib.config.periods.Period instance at 0x23bc1b8>]}
|
||||||
|
}
|
||||||
|
|
||||||
|
2013-05-24 02:29:41,698 [INFO]: parsed arguments
|
||||||
|
2013-05-24 02:29:41,698 [INFO]: reading config...
|
||||||
|
2013-05-24 02:29:41,700 [DEBUG]: parsed config: <lib.config.config.Config instance at 0xb2ba28>
|
||||||
|
2013-05-24 02:29:41,700 [INFO]: HostGroup: all threshold: 10 parent: network
|
||||||
|
members: {
|
||||||
|
Member Id: admin Name: Peter Hansen Filters: Filter('Name: sms', 'Command: /usr/bin/warn_the_admin_sms +23345567 @+message', 'Priority: 0')Filter('Name: email', 'Command: /usr/bin/warn_the_admin_mail admin@systemchaos.org @+message', 'Priority: 1')
|
||||||
|
}
|
||||||
|
hosts: {
|
||||||
|
Host('Name: hanez', 'access: www.hanez.org', 'HostServices: {Service('Name: load', 'Command: ssh www.hanez.org uptime, 'Parser: ')warning: 8.0critical: 12.0
|
||||||
|
Service('Name: processcountbyname', 'Command: ssh www.hanez.org ps ax | grep java | wc -l, 'Parser: ')warning: 100critical: 200
|
||||||
|
Service('Name: htmlcontent', 'Command: wget -qO- @host/test.php, 'Parser: ')
|
||||||
|
Service('Name: discusage', 'Command: ssh www.hanez.org df -a /dev/sda1, 'Parser: ')warning: 80%critical: 90%
|
||||||
|
Service('Name: discusage', 'Command: ssh www.hanez.org df -a /dev/sda2, 'Parser: ')warning: 70%critical: 90%
|
||||||
|
Service('Name: discusage', 'Command: ssh www.hanez.org df -a /dev/sda3, 'Parser: ')warning: 70GBcritical: 90GB
|
||||||
|
Service('Name: processcount', 'Command: ssh www.hanez.org ps ax | wc -l, 'Parser: ')warning: 3000critical: 3800
|
||||||
|
Service('Name: fileage', 'Command: ssh www.hanez.org ls -l /var/backup/server_1, 'Parser: ')warning: 2critical: 3
|
||||||
|
Service('Name: dirsize', 'Command: ssh www.hanez.org du -chs /var/log, 'Parser: ')warning: 30000000critical: 40000000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
services: {
|
||||||
|
HostgroupService { Service('Name: load', 'Command: ssh @host uptime, 'Parser: '), [<lib.config.periods.Period instance at 0xb821b8>]}
|
||||||
|
HostgroupService { Service('Name: discusage', 'Command: ssh @host df -a @device, 'Parser: df'), [<lib.config.periods.Period instance at 0xb821b8>]}
|
||||||
|
HostgroupService { Service('Name: ping', 'Command: ping @host, 'Parser: '), [<lib.config.periods.Period instance at 0xb821b8>]}
|
||||||
|
}
|
||||||
|
|
||||||
|
2013-05-24 02:31:20,124 [INFO]: parsed arguments
|
||||||
|
2013-05-24 02:31:20,124 [INFO]: reading config...
|
||||||
|
2013-05-24 02:31:20,124 [WARNING]: filter: phone is not defined in member Peter Hansen
|
||||||
|
2013-05-24 02:31:20,126 [WARNING]: filter: phone is not defined in member Jens Larssen
|
||||||
|
2013-05-24 02:31:20,127 [WARNING]: filter: phone is not defined in member Hans-Peter Hansen (CEO)
|
||||||
|
2013-05-24 02:31:20,127 [DEBUG]: parsed config: <lib.config.config.Config instance at 0x1d29a70>
|
||||||
|
2013-05-24 02:31:20,127 [INFO]: HostGroup: all threshold: 10 parent: network
|
||||||
|
members: {
|
||||||
|
Member Id: admin Name: Peter Hansen Filters: Filter('Name: sms', 'Command: /usr/bin/warn_the_admin_sms +23345567 @+message', 'Priority: 0')Filter('Name: email', 'Command: /usr/bin/warn_the_admin_mail admin@systemchaos.org @+message', 'Priority: 1')
|
||||||
|
}
|
||||||
|
hosts: {
|
||||||
|
Host('Name: hanez', 'access: www.hanez.org', 'HostServices: {Service('Name: load', 'Command: ssh www.hanez.org uptime, 'Parser: ')warning: 8.0critical: 12.0
|
||||||
|
Service('Name: processcountbyname', 'Command: ssh www.hanez.org ps ax | grep java | wc -l, 'Parser: ')warning: 100critical: 200
|
||||||
|
Service('Name: htmlcontent', 'Command: wget -qO- @host/test.php, 'Parser: ')
|
||||||
|
Service('Name: discusage', 'Command: ssh www.hanez.org df -a /dev/sda1, 'Parser: ')warning: 80%critical: 90%
|
||||||
|
Service('Name: discusage', 'Command: ssh www.hanez.org df -a /dev/sda2, 'Parser: ')warning: 70%critical: 90%
|
||||||
|
Service('Name: discusage', 'Command: ssh www.hanez.org df -a /dev/sda3, 'Parser: ')warning: 70GBcritical: 90GB
|
||||||
|
Service('Name: processcount', 'Command: ssh www.hanez.org ps ax | wc -l, 'Parser: ')warning: 3000critical: 3800
|
||||||
|
Service('Name: fileage', 'Command: ssh www.hanez.org ls -l /var/backup/server_1, 'Parser: ')warning: 2critical: 3
|
||||||
|
Service('Name: dirsize', 'Command: ssh www.hanez.org du -chs /var/log, 'Parser: ')warning: 30000000critical: 40000000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
services: {
|
||||||
|
HostgroupService { Service('Name: load', 'Command: ssh @host uptime, 'Parser: '), [<lib.config.periods.Period instance at 0x1d35bd8>]}
|
||||||
|
HostgroupService { Service('Name: discusage', 'Command: ssh @host df -a @device, 'Parser: df'), [<lib.config.periods.Period instance at 0x1d35bd8>]}
|
||||||
|
HostgroupService { Service('Name: ping', 'Command: ping @host, 'Parser: '), [<lib.config.periods.Period instance at 0x1d35bd8>]}
|
||||||
|
}
|
||||||
|
|
||||||
|
2013-05-24 02:34:58,280 [INFO]: parsed arguments
|
||||||
|
2013-05-24 02:34:58,280 [INFO]: reading config...
|
||||||
|
2013-05-24 02:34:58,281 [WARNING]: filter: phone is not defined in member Peter Hansen
|
||||||
|
2013-05-24 02:34:58,281 [WARNING]: filter: phone is not defined in member Jens Larssen
|
||||||
|
2013-05-24 02:34:58,281 [WARNING]: filter: phone is not defined in member Hans-Peter Hansen (CEO)
|
||||||
|
2013-05-24 02:34:58,282 [WARNING]: undefined parameter: content in host hanez from service htmlcontent
|
||||||
|
2013-05-24 02:34:58,282 [DEBUG]: parsed config: <lib.config.config.Config instance at 0x257ea28>
|
||||||
|
2013-05-24 02:34:58,282 [INFO]: HostGroup: all threshold: 10 parent: network
|
||||||
|
members: {
|
||||||
|
Member Id: admin Name: Peter Hansen Filters: Filter('Name: sms', 'Command: /usr/bin/warn_the_admin_sms +23345567 @+message', 'Priority: 0')Filter('Name: email', 'Command: /usr/bin/warn_the_admin_mail admin@systemchaos.org @+message', 'Priority: 1')
|
||||||
|
}
|
||||||
|
hosts: {
|
||||||
|
Host('Name: hanez', 'access: www.hanez.org', 'HostServices: {Service('Name: load', 'Command: ssh www.hanez.org uptime, 'Parser: ')warning: 8.0critical: 12.0
|
||||||
|
Service('Name: processcountbyname', 'Command: ssh www.hanez.org ps ax | grep java | wc -l, 'Parser: ')warning: 100critical: 200
|
||||||
|
Service('Name: htmlcontent', 'Command: wget -qO- @host/test.php, 'Parser: ')
|
||||||
|
Service('Name: discusage', 'Command: ssh www.hanez.org df -a /dev/sda1, 'Parser: ')warning: 80%critical: 90%
|
||||||
|
Service('Name: discusage', 'Command: ssh www.hanez.org df -a /dev/sda2, 'Parser: ')warning: 70%critical: 90%
|
||||||
|
Service('Name: discusage', 'Command: ssh www.hanez.org df -a /dev/sda3, 'Parser: ')warning: 70GBcritical: 90GB
|
||||||
|
Service('Name: processcount', 'Command: ssh www.hanez.org ps ax | wc -l, 'Parser: ')warning: 3000critical: 3800
|
||||||
|
Service('Name: fileage', 'Command: ssh www.hanez.org ls -l /var/backup/server_1, 'Parser: ')warning: 2critical: 3
|
||||||
|
Service('Name: dirsize', 'Command: ssh www.hanez.org du -chs /var/log, 'Parser: ')warning: 30000000critical: 40000000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
services: {
|
||||||
|
HostgroupService { Service('Name: load', 'Command: ssh @host uptime, 'Parser: '), [<lib.config.periods.Period instance at 0x258ab90>]}
|
||||||
|
HostgroupService { Service('Name: discusage', 'Command: ssh @host df -a @device, 'Parser: df'), [<lib.config.periods.Period instance at 0x258ab90>]}
|
||||||
|
HostgroupService { Service('Name: ping', 'Command: ping @host, 'Parser: '), [<lib.config.periods.Period instance at 0x258ab90>]}
|
||||||
|
}
|
||||||
|
|
||||||
|
2013-05-24 02:44:52,340 [INFO]: parsed arguments
|
||||||
|
2013-05-24 02:44:52,340 [INFO]: starting linspector: reading config...
|
||||||
|
2013-05-24 02:44:52,341 [WARNING]: filter: phone is not defined in member Peter Hansen
|
||||||
|
2013-05-24 02:44:52,341 [WARNING]: filter: phone is not defined in member Jens Larssen
|
||||||
|
2013-05-24 02:44:52,341 [WARNING]: filter: phone is not defined in member Hans-Peter Hansen (CEO)
|
||||||
|
2013-05-24 02:44:52,342 [WARNING]: undefined parameter: content in host hanez from service htmlcontent
|
||||||
|
2013-05-24 02:44:52,342 [DEBUG]: parsed config: <lib.config.config.Config instance at 0x19e6b48>
|
||||||
|
2013-05-24 02:44:52,342 [INFO]: HostGroup: all threshold: 10 parent: network
|
||||||
|
members: {
|
||||||
|
Member Id: admin Name: Peter Hansen Filters: Filter('Name: sms', 'Command: /usr/bin/warn_the_admin_sms +23345567 @+message', 'Priority: 0')Filter('Name: email', 'Command: /usr/bin/warn_the_admin_mail admin@systemchaos.org @+message', 'Priority: 1')
|
||||||
|
}
|
||||||
|
hosts: {
|
||||||
|
Host('Name: hanez', 'access: www.hanez.org', 'HostServices: {Service('Name: load', 'Command: ssh www.hanez.org uptime, 'Parser: ')warning: 8.0critical: 12.0
|
||||||
|
Service('Name: processcountbyname', 'Command: ssh www.hanez.org ps ax | grep java | wc -l, 'Parser: ')warning: 100critical: 200
|
||||||
|
Service('Name: htmlcontent', 'Command: wget -qO- @host/test.php, 'Parser: ')
|
||||||
|
Service('Name: discusage', 'Command: ssh www.hanez.org df -a /dev/sda1, 'Parser: ')warning: 80%critical: 90%
|
||||||
|
Service('Name: discusage', 'Command: ssh www.hanez.org df -a /dev/sda2, 'Parser: ')warning: 70%critical: 90%
|
||||||
|
Service('Name: discusage', 'Command: ssh www.hanez.org df -a /dev/sda3, 'Parser: ')warning: 70GBcritical: 90GB
|
||||||
|
Service('Name: processcount', 'Command: ssh www.hanez.org ps ax | wc -l, 'Parser: ')warning: 3000critical: 3800
|
||||||
|
Service('Name: fileage', 'Command: ssh www.hanez.org ls -l /var/backup/server_1, 'Parser: ')warning: 2critical: 3
|
||||||
|
Service('Name: dirsize', 'Command: ssh www.hanez.org du -chs /var/log, 'Parser: ')warning: 30000000critical: 40000000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
services: {
|
||||||
|
HostgroupService { Service('Name: load', 'Command: ssh @host uptime, 'Parser: '), [<lib.config.periods.Period instance at 0x1a11758>]}
|
||||||
|
HostgroupService { Service('Name: discusage', 'Command: ssh @host df -a @device, 'Parser: df'), [<lib.config.periods.Period instance at 0x1a11758>]}
|
||||||
|
HostgroupService { Service('Name: ping', 'Command: ping @host, 'Parser: '), [<lib.config.periods.Period instance at 0x1a11758>]}
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue