craplog-CLI/craplog/craplog.py

1316 lines
49 KiB
Python

import os
from sys import argv
from sys import path as libpath
crappath = os.path.abspath(__file__)
crappath = crappath[:crappath.rfind('/')]
libpath.append(crappath[:crappath.rfind('/')])
from time import sleep
from time import perf_counter as timer
from subprocess import run, STDOUT, DEVNULL
from craplib import aux
from crappy.aux import MSG_help, MSG_examples
from crappy.check import makeInitialChecks, checkSessionsDates
from crappy.hashes import bringHashes, storeHashes
from crappy.read import collectLogLines
from crappy.parse import parseLogLines
from crappy.stats import updateGlobals, storeSessions
from crappy.backup import backupOriginals, backupGlobals
class Craplog(object):
"""
Make statistics from Apache2 logs
"""
def __init__(self, args: list ):
"""
Craplog's initializer
"""
# craptool name
self.name = "craplog"
# variables from args
self.use_configs: bool
self.use_arguments: bool
self.less_output: bool
self.more_output: bool
self.use_colors: bool
self.performance: bool
self.auto_delete: bool
self.auto_merge: bool
self.warning_size: float
self.session_stats: bool
self.global_stats: bool
self.access_logs: bool
self.error_logs: bool
self.backup: bool
self.archive_tar: bool
self.archive_zip: bool
self.delete: bool
self.trash: bool
self.shred: bool
self.logs_path: str
self.log_files: list
self.file_selection: bool
self.usage_control: bool
self.access_fields: list
self.ip_whitelist: list
# variables for jobs
self.aborted: bool
self.proceed: bool
self.collection: dict
self.undo_paths: list
self.undo_fails: dict
self.crappath: str
self.statpath: str
# variables for performance
self.start_time: float
self.elapsed_time: float
self.crap_time: float
self.user_time: float
self.parsed_size: int
self.logs_size: int
self.access_size: int
self.errors_size: int
self.whitelist_size: int
self.total_lines: int
self.access_lines: int
self.errors_lines: int
self.whitelist_lines: int
# text messages
self.last_job: str
self.caret_return: int
self.text_colors: dict
self.MSG_elbarto: str
self.LOGO_craplog: str
self.MSG_help: str
self.MSG_examples: str
self.MSG_craplog: str
self.MSG_fin: str
self.TXT_craplog: str
self.TXT_fin: str
# get craplog's path
self.crappath = os.path.abspath(__file__)
self.crappath = self.crappath[:self.crappath.rfind('/')]
self.statpath = "%s/crapstats" %(self.crappath[:self.crappath.rfind('/')])
# initialize variables
self.initVariables()
self.initMessages()
# read configs if not unset
if self.use_configs is True:
self.readConfigs()
# parse arguments if not unset
if self.use_arguments is True:
self.parseArguments( args )
def initVariables(self):
"""
Initialize Craplog's variables
This section can be manually edited to pre-set Craplog
and avoid having to pass arguments every time
"""
################################################################
# START OF THE EDITABLE SECTION
#
# HIERARCHY FOR APPLYING SETTINGS:
# - HARDCODED VARIABLES (THESE ONES)
# - CONFIGURATIONS FILE
# - COMMAND LINE ARGUMENTS
# THE ELEMENTS ON TOP ARE REPLACED BY THE ONES WHICH FOLLOW THEM,
# IF HARDCODED VARIABLES ARE SET TO DO SO
#
# READ THE CONFIGURATIONS FILE AND LOAD THE SETTING
# [ ]
# IF SET TO 'False' MEANS THAT THE SAVED CONFIGS WILL BE IGNORED
self.use_configs = True
#
# USE COMMAND LINE ARGUMENTS
# [ ]
# SETTING THIS VARIABLE TO 'False' MEANS THAT EVERY ARGUMENT WILL BE IGNORED
# ONLY THE MANUAL CONFIGURATION OF THESE VARIABLES WILL BE USED
self.use_arguments = True
#
# REDUCE THE OUTPUT ON SCREEN
# [ -l / --less ]
self.less_output = False
#
# PRINT MORE INFORMATIONS ON SCREEN
# [ -m / --more ]
self.more_output = False
#
# SHOW INFORMATIONS ABOUT THE PERFORMANCE
# [ -p / --performance ]
self.performance = False
#
# USE COLORS WHEN PRINTING TEXT ON SCREEN
# CAN BE DISABLED PASSING [ --no-colors ]
self.use_colors = True
#
# AUTOMATICALLY DELETE FILES WHEN NEEDED
# [ --auto-delete ]
# USE WITH CAUTION, THIS APPLIES IN EVERY CIRCUMSTANCE
# INCLUDES: ORIGINAL LOG FILES, CONFLICT FILES/FOLDERS
self.auto_delete = False
#
# AUTOMATICALLY MERGE SESSIONS STATISTICS WITH THE SAME DATE
# [ --auto-merge ]
# IF SOME OF THE NEWELY PARSED LOGS HAVE THE SAME DATE OF AN
# ALREADY STORED SESSION, MERGE THE RELATIVE LINES
self.auto_merge = False
#
# A WARNING IS EMITTED IF THE SIZE AN INPUT FILE OVERTAKES THIS LIMIT
# [ --max-size ]
# IN MB (MegaBytes)
self.warning_size = 100.0
#
# STORE SESSION STATISTICS OF THE PARSED DATA
# CAN BE DISABLED PASSING [ -gO / --only-globals ]
self.session_stats = True
#
# UPDATE GLOBAL STATISTICS WITH THE PARSED DATA
# CAN BE DISABLED PASSING [ -gA / --avoid-globals ]
self.global_stats = True
#
# MAKE STATISTICS FROM ACCESS LOGS
# [ ]
# CAN BE DISABLED PASSING [ --only-errors ]
self.access_logs = True
#
# MAKE STATISTICS FROM ERROR LOGS
# [ -e / --errors ]
self.error_logs = False
#
# MAKE A BACKUP COPY OF THE ORIGINAL LOG FILES (AS THEY ARE)
# [ -b / --backup ]
# MUST BE SET TO True IF AN ARCHIVE CHOICE IS True
self.backup = False
#
# ARCHIVE THE BACKUP AS tar.gz
# [ -bT / --backup-tar ]
# gzip COMPRESSED tar ARCHIVE
self.archive_tar = False
#
# ARCHIVE THE BACKUP AS zip
# [ -bZ / --backup-zip ]
# TRIES TO COMPRESS THE ARCHIVE WITH THE MAX COMPRESSION LEVEL
# STORES AS NORMAL zip IF THE PREVIOUS FAILS
self.archive_zip = False
#
# DELETE THE ORIGINAL LOG FILES WHEN DONE
# [ -dO / --delete-originals ]
# IF THE PROCESS FAILS BEFORE THE DELETE STEP, DELETION WILL BE SKIPPED
# AFTER THE DELETE STEP, THE PROCESS WILL NO MORE BE REVERSIBLE
self.delete = False
#
# MOVE FILES TO TRASH INSTEAD OF COMPLETELY REMOVING THEM
# [ --trash ]
# DOESN'T APPLY TO CONFLICT FILES, WHICH WILL BE REMOVED (OR SHREDED)
self.trash = False
#
# THE DIRECTORY USED AS TRASH BY YOUR SYSTEM
# CAN BE PASSED FOLLOWING THE [ --trash <path> ] OPTION
# DEFAULT TO: ~/.local/share/Trash/files/
self.trash_path = "~/.local/share/Trash/files/"
#
# SHRED FILES INSTEAD OF SIMPLY REMOVING THEM
# [ --shred ]
self.shred = False
#
# THE DIRECTORY CONTAINING THE LOGS FILES
# [ -P / --logs-path ]
# WHEN PASSING ARGUMENTS, THE OPTION MUST BE FOLLOWED BY THE PATH
self.logs_path = "/var/log/apache2"
#
# THE LIST OF LOGS FILES TO USE
# [ -F / --log-files ]
# WHEN PASSING ARGUMENTS:
# - THE OPTION MUST BE FOLLOWED BY THE LIST OF FILES
# - FILES MUST BE PASSED AS NAMES ONLY, NOT PATHS
# - ' ' (WITHESPACE) HAVE TO BE USED AS SEPARATOR BETWEEN NAMES
self.log_files = ["access.log.1"]
#
# True ONLY WHEN USING A CUSTOM LIST OF FILES !-> FROM ARGUMENTS <-!
# [ ]
self.file_selection = False
#
# STORE A HASH OF EVERY PARSED FILE TO AVOID PARSING THEM TWICE
# [ ]
# THE HASH ALGORITHM IS sha256
# PLEASE NOTICE THAT THIS CANNOT BE USED TO TRACK YOU OR YOUR FILES,
# THE HASH IS IRREVERSIBLE AND CAN'T THEREFORE BE USED TO HARM YOUR PRIVACY
self.usage_control = True
#
# LIST OF FIELDS TO BE USED WHILE PARSING ACCESS LOGS' LINES
# [ -A / --access-fields ]
# WHEN PASSING ARGUMENTS:
# - THE OPTION MUST BE FOLLOWED BY THE LIST OF FIELDS
# - FIELDS MUST BE PASSED AS ABBREVIATIONS
# - ' ' (WITHESPACE) HAVE TO BE USED AS SEPARATOR
self.access_fields = ["IP", "REQ", "RES", "UA"]
#
# LOG LINES FROM THESE IPs WILL BE SKIPPED
# [ -W / --ip-whitelist ]
# VIEW 'README.md' FOR MORE INFORMATIONS
# WHEN PASSING ARGUMENTS:
# - THE OPTION MUST BE FOLLOWED BY THE LIST OF IPs
# - ' ' (WITHESPACE) HAVE TO BE USED AS SEPARATOR
self.ip_whitelist = ["::1"]
#
# END OF THE EDITABLE SECTION
################################################################
#
#
# DO NOT MODIFY THE FOLLOWING VARIABLES
self.collection = {}
self.undo_paths = []
self.undo_fails = []
self.hashes = []
self.aborted = False
self.proceed = True
self.elapsed_time = 0.
self.crap_time = 0.
self.user_time = 0.
self.parsed_size = 0
self.logs_size = 0
self.access_size = 0
self.errors_size = 0
self.whitelist_size = 0
self.total_lines = 0
self.access_lines = 0
self.errors_lines = 0
self.whitelist_lines = 0
def readConfigs(self):
"""
Read the saved configuration
"""
path = "%s/crapconfs/craplog.crapconf" %(self.crappath[:self.crappath.rfind('/')])
if os.path.exists( path ) is False:
# leave this normal yellow, it's secondary and doesn't need a real attention
if self.less_output is False:
print("\n{warn}Warning{white}[{grey}configs{white}]{warn}>{default} {yellow}configurations file not found\n"\
.format(**self.text_colors))
sleep(1)
else:
with open(path,'r') as f:
tmp = f.read().strip().split('\n')
configs = []
for f in tmp:
f = f.strip()
if f == ""\
or f[0] == "#":
continue
configs.append(f)
# check the length
if len(configs) != 25:
self.printJobFailed()
print("\n{err}Error{white}[{grey}configs{white}]{red}>{default} invalid number of lines: {rose}%s{default}"\
.format(**self.text_colors)\
%( len(configs) ))
if self.less_output is False:
print(" if you have manually edited the configurations file, please un-do the changes")
print(" else, please report this issue")
print()
self.exitAborted()
# apply the configs
self.use_configs = bool(int(configs[0]))
if self.use_configs is True:
self.use_arguments = bool(int(configs[1]))
self.less_output = bool(int(configs[2]))
self.more_output = bool(int(configs[3]))
self.use_colors = bool(int(configs[4]))
self.performance = bool(int(configs[5]))
self.auto_delete = bool(int(configs[6]))
self.auto_merge = bool(int(configs[7]))
self.warning_size = float(configs[8])
self.session_stats = bool(int(configs[9]))
self.global_stats = bool(int(configs[10]))
self.access_logs = bool(int(configs[11]))
self.error_logs = bool(int(configs[12]))
self.backup = bool(int(configs[13]))
self.archive_tar = bool(int(configs[14]))
self.archive_zip = bool(int(configs[15]))
self.delete = bool(int(configs[16]))
self.trash = bool(int(configs[17]))
self.shred = bool(int(configs[18]))
self.logs_path = configs[19]
self.log_files = configs[20].split(' ')
self.file_selection = bool(int(configs[21]))
self.usage_control = bool(int(configs[22]))
self.access_fields = configs[23].split(' ')
self.ip_whitelist = configs[24].split(' ')
self.initMessages()
# check log files
tmp = [f.strip() for f in self.log_files]
self.log_files = []
for f in tmp:
if f != "":
self.log_files.append( f )
# check access fields
tmp = [f.strip() for f in self.access_fields]
self.access_fields = []
for f in tmp:
if f == "":
continue
f = f.upper()
if tmp.count( f ) > 1:
self.printJobFailed()
print("\n{err}Error{white}[{grey}configs{white}]{red}>{default} you have inserted the same field twice: {rose}%s{default}\n"\
.format(**self.text_colors)\
%( f ))
self.exitAborted()
elif f not in ["IP","UA","REQ","RES"]:
self.printJobFailed()
print("\n{err}Error{white}[{grey}configs{white}]{red}>{default} invalid field for access logs: {rose}%s{default}\n"\
.format(**self.text_colors)\
%( f ))
self.exitAborted()
self.access_fields.append( f )
# check whitelist
tmp = [f.strip() for f in self.ip_whitelist]
self.ip_whitelist = []
for f in tmp:
if f != "":
self.ip_whitelist.append( f )
def initMessages(self):
"""
Bring message strings
"""
self.last_job = ""
self.caret_return = 0
if self.use_colors is True:
self.text_colors = aux.colors()
else:
self.text_colors = aux.no_colors()
self.MSG_elbarto = aux.elbarto()
self.LOGO_craplog = aux.LOGO_craplog()
self.MSG_help = MSG_help( self.text_colors )
self.MSG_examples = MSG_examples( self.text_colors )
self.MSG_craplog = aux.MSG_craplog( self.text_colors )
self.MSG_fin = aux.MSG_fin( self.text_colors )
self.TXT_craplog = aux.TXT_craplog( self.text_colors )
self.TXT_fin = aux.TXT_fin( self.text_colors )
def parseArguments(self, args: list ):
"""
Finalize Craplog's variables (if not manually unset)
"""
n_args = len(args)-1
i = 0
while i < n_args:
i += 1
arg = args[i]
if arg in ["","log","craplog"]:
continue
# elB4RTO
elif arg in ["elB4RTO","elbarto","-elbarto-"]:
print("\n%s\n" %( self.MSG_elbarto ))
exit()
# help
elif arg in ["help", "-h", "--help"]:
print("\n%s\n\n%s\n\n%s\n" %( self.LOGO_craplog, self.MSG_help, self.MSG_examples ))
exit()
elif arg == "--examples":
print("\n%s\n\n%s\n" %( self.LOGO_craplog, self.MSG_examples ))
exit()
# auxiliary arguments
elif arg in ["-l", "--less"]:
self.less_output = True
elif arg in ["-m", "--more"]:
self.more_output = True
elif arg in ["-p", "--performance"]:
self.performance = True
elif arg == "--no-colors":
self.use_colors = False
self.initMessages()
# automation arguments
elif arg == "--auto-delete":
self.auto_delete = True
elif arg == "--auto-merge":
self.auto_merge = True
# file size limit
elif arg == "--warning-size":
if i+1 > n_args\
or ( args[i+1].startswith("--")\
or (args[i+1].startswith("-") and not args[i+1][1].isdigit())):
self.warning_size = None
else:
i += 1
self.warning_size = args[i]
# job arguments
elif arg in ["-e", "--errors"]:
self.error_logs = True
elif arg in ["-eO", "--only-errors"]:
self.access_logs = False
self.error_logs = True
elif arg in ["-gO", "--only-globals"]:
self.session_stats = False
elif arg in ["-gA", "--avoid-globals"]:
self.global_stats = False
elif arg in ["-b", "--backup"]:
self.backup = True
elif arg in ["-bT", "--backup-tar"]:
self.backup = True
self.archive_tar = True
elif arg in ["-bZ", "--backup-zip"]:
self.backup = True
self.archive_zip = True
elif arg in ["-dO", "--delete-originals"]:
self.delete = True
elif arg == "--shred":
self.shred = True
# user defined arguments
elif arg == "--trash":
self.trash = True
if i+1 <= n_args\
and not args[i+1].startswith("-"):
i += 1
self.trash_path = args[i]
elif arg in ["-P", "--logs-path"]:
if i+1 > n_args\
or args[i+1].startswith("-"):
self.logs_path = ""
else:
i += 1
self.logs_path = args[i]
elif arg in ["-F", "--log-files"]:
self.file_selection = True
self.log_files = []
while True:
if i+1 > n_args\
or args[i+1].startswith("-"):
break
else:
i += 1
self.log_files.append( args[i] )
elif arg in ["-A", "--access-fields"]:
self.access_fields = []
while True:
if i+1 > n_args\
or args[i+1].startswith("-"):
break
else:
i += 1
self.access_fields.append( args[i] )
elif arg in ["-W", "--ip-whitelist"]:
self.ip_whitelist = []
while True:
if i+1 > n_args\
or args[i+1].startswith("-"):
break
else:
i += 1
self.ip_whitelist.append( args[i] )
else:
print("{err}Error{white}[{grey}argument{white}]{red}>{default} not an available option: {rose}%s{default}"\
.format(**self.text_colors)\
%(arg))
if self.more_output is True:
print(" use {cyan}craplog --help{default} to view an help screen"\
.format(**self.text_colors))
exit("")
def welcomeMessage(self):
"""
Print the welcome message
"""
if self.less_output is False:
print("\n%s\n" %( self.MSG_craplog ))
if self.more_output is True:
print("Use {cyan}craplog --help{default} to view an help screen"\
.format(**self.text_colors))
if self.auto_delete is self.auto_merge is True:
print("{yellow}Auto-Delete{default} and {yellow}Auto-Merge{default} are {bold}ON{default}"\
.format(**self.text_colors))
else:
if self.auto_delete is True:
print("{yellow}Auto-Delete{default} is {bold}ON{default}"\
.format(**self.text_colors))
if self.auto_merge is True:
print("{yellow}Auto-Merge{default} is {bold}ON{default}"\
.format(**self.text_colors))
print()
sleep(1)
else:
print("{bold}%s"\
.format(**self.text_colors)\
%( self.TXT_craplog ))
def exitMessage(self, no_perf:bool=False ):
"""
Print the exit message
"""
if self.performance is True\
and no_perf is False:
self.printOverallPerformance()
if self.less_output is False:
print("\n%s\n" %( self.MSG_fin ))
else:
print("{bold}%s"\
.format(**self.text_colors)\
%( self.TXT_fin ))
def printJob(self, message: str ):
"""
Print a job-relative message
"""
self.last_job = "{bold}%s {default}{paradise}...{default} "\
.format(**self.text_colors)\
%( message )
print(self.last_job, end="", flush=True)
def reprintJob(self):
"""
Print a job-relative message
"""
self.caret_return = 0
print(self.last_job, end="", flush=True)
def printJobHalted(self):
"""
Print the job has been halted
"""
if self.last_job != "":
self.restoreCaret()
print("{orange}Halted{default}"\
.format(**self.text_colors),
end="", flush=True )
def printJobDone(self):
"""
Print the job is done
"""
print("{grass}Done{default}"\
.format(**self.text_colors))
self.last_job = ""
def printJobFailed(self):
"""
Print the job failed
"""
if self.proceed is True:
self.proceed = False
if self.caret_return != 0:
self.restoreCaret()
print("{rose}Failed{default}"\
.format(**self.text_colors))
self.printElapsedTime()
self.timer_gap = timer()
self.last_job = ""
if len(self.undo_paths) > 0:
self.undoChanges()
def printCaret(self, message: str ):
"""
Print the message and update the caret for a restore
"""
if self.more_output is True:
print("{yellow}%s{default}"\
.format(**self.text_colors)\
%( message ),
end="", flush=True )
self.caret_return = len(message)
def restoreCaret(self):
"""
Restore the caret to the previous position
"""
if self.more_output is True:
print("%s%s%s"\
%( "\b"*self.caret_return, " "*self.caret_return, "\b"*self.caret_return ),
end="", flush=True )
self.caret_return = 0
def printAborted(self):
"""
Print the abortion message
"""
self.aborted = True
if self.less_output is False:
print()
print("{err}CRAPLOG ABORTED{default}"\
.format(**self.text_colors))
if self.less_output is False:
print()
def exitAborted(self):
"""
Print the abortion message and exit
"""
self.aborted = True
self.finalCleanUp()
if self.less_output is False:
print()
print("{err}CRAPLOG ABORTED{default}"\
.format(**self.text_colors))
if self.less_output is False:
print()
exit()
def printElapsedTime(self):
"""
Print the time elapsed since the last gap
"""
if self.more_output is True\
and self.performance is True:
elapsed = timer() - self.time_gap
if elapsed <= 60:
msg = "%.2f {white}s"\
.format(**self.text_colors)\
%( elapsed )
else:
msg = "%d {white}m{pink} %d {white}s"\
.format(**self.text_colors)\
%( int(elapsed/60), (elapsed%60) )
print("{grey}┖┄{purple}elapsed time{paradise}:{pink} %s{default}"\
.format(**self.text_colors)\
%( msg ))
def printOverallPerformance(self):
"""
Print overall performance details
"""
self.elapsed_time = timer() - self.start_time
self.crap_time = self.elapsed_time - self.user_time
if self.less_output is False:
print("{pink}Total size parsed{white}:{paradise} %.2f {white}MB{default}"\
.format(**self.text_colors)\
%( self.parsed_size / 1048576 ))
if self.more_output is True:
print("{grey}┠┄{pink}total logs size{white}:{paradise} %.2f {white}MB{default}"\
.format(**self.text_colors)\
%( self.logs_size / 1048576 ))
print("{grey}┃ └┄{pink}total number of lines{white}:{paradise} %d {default}"\
.format(**self.text_colors)\
%( self.total_lines ))
if self.less_output is False:
print("{grey}┖┄{pink}total used logs size{white}:{paradise} %.2f {white}MB{default}"\
.format(**self.text_colors)\
%( (self.access_size+self.errors_size) / 1048576 ))
if self.more_output is True:
print("{grey} ┠┄{pink}total number of used lines{white}:{paradise} %d {default}"\
.format(**self.text_colors)\
%( (self.access_lines+self.errors_lines) ))
if self.access_logs is True:
tree = "" # for errors
print("{grey} ┖─┬┄{pink}used access logs size{white}:{paradise} %.2f {white}MB{default}"\
.format(**self.text_colors)\
%( self.access_size / 1048576 ))
print("{grey} │ └┄{pink}number of access lines{white}:{paradise} %d {default}"\
.format(**self.text_colors)\
%( self.access_lines ))
else:
tree = "┖─┬"
if self.error_logs is True:
print("{grey} %s{pink}used error logs size{white}:{paradise} %.2f {white}MB{default}"\
.format(**self.text_colors)\
%( tree, (self.errors_size / 1048576) ))
print("{grey} │ └┄{pink}number of error lines{white}:{paradise} %d {default}"\
.format(**self.text_colors)\
%( self.errors_lines ))
if len(self.ip_whitelist) > 1\
or (len(self.ip_whitelist) == 1
and self.ip_whitelist[0] != "::1"):
print("{grey} ├┄{pink}whitelisted logs size{white}:{paradise} %.2f {white}MB{default}"\
.format(**self.text_colors)\
%( self.whitelist_size / 1048576 ))
print("{grey} │ └┄{pink}number of whitelisted lines{white}:{paradise} %d {default}"\
.format(**self.text_colors)\
%( self.whitelist_lines ))
self.ip_whitelist.clear()
print("{grey} └┄{pink}discarded logs size{white}:{paradise} %.2f {white}MB{default}"\
.format(**self.text_colors)\
%( (self.logs_size - (self.access_size+self.errors_size)) / 1048576 ))
print("{grey} └┄{pink}number of discarded lines{white}:{paradise} %d {default}"\
.format(**self.text_colors)\
%( self.total_lines - (self.access_lines+self.errors_lines) ))
print("{pink}Total time elapsed{white}:{paradise} %d {white}m{paradise} %d {white}s{default}"\
.format(**self.text_colors)\
%( int(self.elapsed_time/60), (self.elapsed_time%60) ))
if self.more_output is True:
print("{grey}┠┄{pink}time used by craplog{white}:{paradise} %d {white}m{paradise} %d {white}s{default}"\
.format(**self.text_colors)\
%( int(self.crap_time/60), (self.crap_time%60) ))
print("{grey}┖┄{pink}time used by you{white}:{paradise} %d {white}m{paradise} %d {white}s{default}"\
.format(**self.text_colors)\
%( int(self.user_time/60), (self.user_time%60) ))
print("{pink}Overall performance{white}:{paradise} %.2f {white}KB/s{default}"\
.format(**self.text_colors)\
%( (self.parsed_size / 1024) / self.crap_time ))
if self.more_output is True:
real_lines = (self.access_lines+self.errors_lines)
if (real_lines / self.crap_time) < real_lines:
print("{grey}┖┄{pink}over lines{white}:{paradise} %.2f {white}lines/sec{default}"\
.format(**self.text_colors)\
%( real_lines / self.crap_time ))
if (self.crap_time/60) > 1:
print("{grey}{pink}Alternative overall{white}:{paradise} %.2f {white}MB/m{default}"\
.format(**self.text_colors)\
%( (self.parsed_size / 1048576) / (self.crap_time/60) ))
if self.more_output is True:
if (real_lines / (self.crap_time/60)) < real_lines:
print("{grey}┖┄{pink}over lines{white}:{paradise} %.2f {white}lines/min{default}"\
.format(**self.text_colors)\
%( real_lines / (self.crap_time/60) ))
def removeEntry(self, path: str ):
"""
Remove an entry (file/folder) accordingly to settings
"""
del_mode = "remove"
del_mode_aux = ""
if self.trash is True:
del_mode = "move"
del_mode_aux = " to trash"
elif self.shred is True:
del_mode = "shred"
parent = path[:path.rfind('/')]
entry = path[len(parent)+1:]
if os.path.isdir( path ):
entry_type = "folder"
elif os.path.isfile( path ):
entry_type = "file"
else:
# unknown type
self.printJobFailed()
print("\n{err}Error{white}[{grey}type{white}]{red}>{default} the entry is not a directory, nor a file: {grass}%s/{rose}%s{default}"\
.format(**self.text_colors)\
%( parent, entry ))
if self.more_output is True:
print(" ok, that was unexpected")
print(" please manually check it and consider reporting this issue")
print()
# check the type
return_code = 0
if os.path.isfile( path ):
# is a file
if self.trash is True:
return_code = run(
["mv", path, self.trash_path],
stdout=DEVNULL,
stderr=STDOUT)\
.returncode
elif self.shred is True:
return_code = run(
["shred", "-uvz", path],
stdout=DEVNULL,
stderr=STDOUT)\
.returncode
else:
return_code = run(
["rm", path],
stdout=DEVNULL,
stderr=STDOUT)\
.returncode
if return_code == 1:
self.printJobFailed()
print("\n{err}Error{white}[{grey}file{white}]{red}>{default} unable to %s this %s%s: {grass}%s/{rose}%s{default}"\
.format(**self.text_colors)\
%( del_mode, entry_type, del_mode_aux, parent, entry ))
if self.more_output is True:
print(" the error is most-likely caused by a lack of permissions")
print(" please proceed manually")
print()
elif os.path.isdir( path ):
# is a folder
if self.trash is True:
return_code = run(
["mv", path, self.trash_path],
stdout=DEVNULL,
stderr=STDOUT)\
.returncode
else:
if self.shred is True:
# recursively rename the folder with zeroes
new_name = "0"*(len(entry))
if len(new_name) < 4:
new_name = "0"*4
new_path = path
while len(new_name) > 1:
old_path = new_path
new_name = "0"*(len(new_name)-1)
new_path = "%s/%s" %( parent, new_name )
return_code = run(
["mv", path, new_path],
stdout=DEVNULL,
stderr=STDOUT)\
.returncode
if return_code != 0:
break
path = new_path
if return_code == 0:
# delete the folder
return_code = run(
["rmdir", path],
stdout=DEVNULL,
stderr=STDOUT)\
.returncode
if return_code == 1:
self.printJobFailed()
print("\n{err}Error{white}[{grey}folder{white}]{red}>{default} unable to %s this %s%s: {grass}%s/{rose}%s{default}"\
.format(**self.text_colors)\
%( del_mode, entry_type, del_mode_aux, parent, entry ))
if self.more_output is True:
print(" the error is most-likely caused by a non-empty folder")
print(" or by a lack of permissions")
print(" please manually remove it and retry")
print()
else:
# unknown type
self.printJobFailed()
print("\n{err}Error{white}[{grey}type{white}]{red}>{default} the entry is not a directory, nor a file: {grass}%s/{rose}%s{default}"\
.format(**self.text_colors)\
%( parent, entry ))
if self.more_output is True:
print(" ok, that was unexpected")
print(" please manually check it and consider reporting this issue")
print()
def renameEntry(self, path: str, new_path: str):
"""
Rename an entry (file/folder)
"""
try:
os.rename( path, new_path )
except:
parent = path[:path.rfind('/')]
entry = path[len(parent)+1:]
if os.path.isdir( path ):
entry_type = "folder"
elif os.path.isfile( path ):
entry_type = "file"
else:
# unknown type
self.printJobFailed()
print("\n{err}Error{white}[{grey}type{white}]{red}>{default} the entry is not a directory, nor a file: {grass}%s/{rose}%s{default}"\
.format(**self.text_colors)\
%( parent, entry ))
if self.more_output is True:
print(" ok, that was unexpected")
print(" please manually check it and consider reporting this issue")
print()
# print the error message only if not printed yet
if self.proceed is True:
self.printJobFailed()
print("\n{err}Error{white}[{grey}rename{white}]{red}>{default} unable to rename this %s: {grass}%s/{rose}%s{default}"\
.format(**self.text_colors)\
%( entry_type, parent, entry ))
if self.more_output is True:
print(" the error is most-likely caused by a lack of permissions")
print(" please proceed manually")
print()
def removeOriginals(self):
"""
Remove the original log files used
"""
for original_file in self.log_files:
self.removeEntry( "%s/%s" %( self.logs_path, original_file ))
def undoChanges(self):
"""
Un-do changes after a failure
"""
if self.less_output is False:
self.last_job = "Un-doing changes"
self.caret_return = 0
print("{bold}{rose}Un-doing changes {default}{paradise}...{default} "\
.format(**self.text_colors), end="", flush=True)
self.time_gap = timer()
self.proceed = True
self.undo_fails = {'remove':[],'restore':[]}
for path in reversed(self.undo_paths):
if path.endswith(".bak"):
# delete the new file
old_path = path[:-4]
if os.path.exists( old_path ):
self.removeEntry( old_path )
if self.proceed is False:
self.undo_fails['remove'].append( old_path )
self.undo_fails['restore'].append( path )
self.proceed = True
continue
# and restore the backup
self.renameEntry( path, old_path )
if self.proceed is False:
self.undo_fails['restore'].append( old_path )
self.proceed = True
else:
# delete the newly created entry
self.removeEntry( path )
if self.proceed is False:
self.undo_fails['remove'].append( path )
self.proceed = True
if len(self.undo_fails['remove']) > 0\
or len(self.undo_fails['restore']) > 0:
# print failures
for action, paths in self.undo_fails.items():
if len(paths) == 0:
break
col1 = "\033[91m"
col2 = "\033[1;31m"
if action == "restore":
col1 = "\033[93m"
col2 = "\033[1;33m"
if self.use_colors is False:
col1 = ""
col2 = ""
print("\n{bold}Failed to %s%s{default}:"\
.format(**self.text_colors)\
%( col1, action ))
for path in paths:
print(" - {green}%s/%s%s{default}"\
.format(**self.text_colors)\
%( path[:path.rfind('/')], col2, path[path.rfind('/')+1:] ))
print()
if self.stage == 1:
# only during the crapstats stage
print("{bold}{rose}Changes to the crapstats has been discarded{default}"\
.format(**self.text_colors))
if len(self.undo_fails['remove']) > 0:
print("Please manually remove the files in the remove list before to run craplog again")
if self.more_output is True:
print(" These files are the result of the process (which failed) and must be deleted")
if len(self.undo_fails['restore']) > 0:
print("Please manually restore the files in the restore list before to run craplog again")
if self.more_output is True:
print(" These files are copies of the previous files and thus must be restored")
if self.less_output is False:
print(" You can restore a file by deleting the trailing '{bold}.bak{default}' extension")
else:
# "successfully" failed
if self.less_output is False:
self.printJobDone()
self.printElapsedTime()
# in any case, clear the lists
self.undo_paths.clear()
self.undo_fails.clear()
def finalizeChanges(self):
"""
Finalize changes if exiting successfully
"""
self.undo_fails = {'remove':[],'restore':[]}
for path in self.undo_paths:
if path.endswith(".bak"):
# delete the backups
self.removeEntry( path )
if self.proceed is False:
self.undo_fails['remove'].append( path )
self.proceed = True
if len(self.undo_fails['remove']) > 0:
printJobFailed()
# print failures
for action, paths in self.undo_fails.items():
if len(paths) == 0:
break
print("\n{bold}Failed to {rose}remove{default} {italic}(safety backups){default}:"\
.format(**self.text_colors)\
%( action ))
for path in paths:
print(" - {green}%s/{warn}%s{default}"\
.format(**self.text_colors)\
%( path[:path.rfind('/')], path[path.rfind('/')+1:] ))
print()
if self.more_output is True:
print("There is no reason to undo everything now")
print("{bold}{blue}Changes to the crapstats will be kept{default}".format(**self.text_colors))
if self.more_output is True:
print("These files are just copies of previous stats and thus can be safely deleted")
if self.less_output is False:
print("It is suggested to manually remove these files before to run craplog again")
else:
# successfully finalized
self.undo_paths.clear()
def finalCleanUp(self):
"""
Clean-up variables
"""
self.collection.clear()
self.undo_paths.clear()
self.undo_fails.clear()
self.log_files.clear()
self.logs_path = ""
self.statpath = ""
self.crappath = ""
def main(self):
"""
Make Craplog do its job
"""
# welcome message
self.welcomeMessage()
# CRAPLOG
self.stage = 0
self.start_time = timer()
if self.more_output is True:
self.printJob("Initializing craplog")
self.time_gap = timer()
# make initial checkings
makeInitialChecks( self )
# retrieve usage-control hashes
bringHashes( self )
if self.more_output is True:
self.printJobDone()
self.printElapsedTime()
print()
if self.less_output is True:
# preventive output
self.printJob("Parsing logs")
# read logs files
if self.more_output is True:
self.printJob("Reading logs")
self.time_gap = timer()
logs_data = collectLogLines( self )
if self.more_output is True:
self.printJobDone()
self.printElapsedTime()
# store usage hashes of log files
self.proceed = True
if self.more_output is True:
self.printJob("Saving usage-hashes")
self.time_gap = timer()
storeHashes( self )
if self.proceed is True\
and self.more_output is True:
self.printJobDone()
self.printElapsedTime()
# parse logs lines
if self.less_output is False:
self.printJob("Parsing logs")
self.time_gap = timer()
parseLogLines( self, logs_data )
if self.less_output is False:
self.printJobDone()
self.printElapsedTime()
if self.less_output is True:
# continuation of the preventive output
self.printJobDone()
# check for the presence of older session statistics with the same date
if self.auto_merge is False:
checkSessionsDates( self )
# from now on a failure will un-do any modification to the crapstats
self.stage = 1
if self.less_output is True:
# next preventive output
self.printJob("Updating statistics")
else:
print()
# store session statistics
if self.session_stats is True:
if self.less_output is False:
self.printJob("Storing session statistics")
self.time_gap = timer()
storeSessions( self )
if self.less_output is False:
self.printJobDone()
self.printElapsedTime()
# update global statistics
if self.global_stats is True:
if self.less_output is False:
self.printJob("Updating global statistics")
self.time_gap = timer()
updateGlobals( self )
if self.less_output is False:
self.printJobDone()
self.printElapsedTime()
# finalize changes
self.proceed = True
if self.more_output is True:
self.printJob("Finalizing changes")
self.time_gap = timer()
self.finalizeChanges()
if self.proceed is True\
and self.more_output is True:
self.printJobDone()
self.printElapsedTime()
if self.less_output is True:
# continuation of the preventive output
self.printJobDone()
# next preventive output
if self.backup is True\
or self.delete is True:
self.printJob("Managing original log files")
else:
print()
# 'proceed' will be used from now on
# failures will only un-do further modifications
self.undo_paths.clear()
self.stage = 2
# make a backup copy of the original logs used
if self.backup is True:
if self.less_output is False:
self.printJob("Backing-up original log files")
self.time_gap = timer()
backupOriginals( self )
if self.proceed is True\
and self.less_output is False:
self.printJobDone()
self.printElapsedTime()
# delete original logs used
if self.delete is True:
if self.proceed is False:
print("{bold}{rose}Backup failed, skipping deletion{default}"
.format(**self.text_colors))
if self.less_output is False:
print()
else:
if self.less_output is False:
self.printJob("Deleting original log files")
self.time_gap = timer()
self.removeOriginals()
if self.proceed is True\
and self.less_output is False:
self.printJobDone()
self.printElapsedTime()
if self.less_output is True:
# continuation of the preventive output
if self.proceed is True\
and (self.backup is True\
or self.delete is True):
self.printJobDone()
# next preventive output
self.printJob("Finalizing")
elif self.backup is True\
or self.delete is True:
print()
# make a backup of the globals
if self.global_stats is True:
if self.more_output is True:
self.printJob("Backing-up global statistics")
self.time_gap = timer()
backupGlobals( self )
if self.proceed is True\
and self.more_output is True:
self.printJobDone()
self.printElapsedTime()
# final clean up
if self.less_output is False:
self.printJob("Cleaning up")
self.time_gap = timer()
# not a real need, I know
self.finalCleanUp()
if self.less_output is False:
self.printJobDone()
self.printElapsedTime()
if self.proceed is True\
and self.less_output is True:
# continuation of the preventive output
self.printJobDone()
else:
print()
# fin
self.exitMessage()
if __name__ == "__main__":
failed = False
craplog = Craplog( argv )
try:
# run craplog
craplog.main()
except (KeyboardInterrupt):
failed = True
if craplog.aborted is False:
print()
if craplog.more_output is True:
print()
except:
failed = True
finally:
if failed is True:
if craplog.aborted is False:
try:
# failing succesfully
if len(craplog.undo_paths) > 0:
craplog.undoChanges()
except:
# complete failure
pass
finally:
craplog.exitAborted()
# successful
del craplog