Mini Shell
Direktori : /opt/sharedrads/ |
|
Current File : //opt/sharedrads/disk_cleanup.py |
#!/opt/imh-python/bin/python3
"""Grand Unified Disk Scanner.
For More Information and Usage:
http://wiki.inmotionhosting.com/index.php?title=RADS#disk_cleanup.py"""
import logging
import sys
from datetime import timedelta
from pathlib import Path
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
import pp_api
import yaml
from cpapis import whmapi1, CpAPIError
import rads
from guds_modules.change import run_disk_change
from guds_modules.aux import run_aux
from guds_modules.base import ModuleBase
from guds_modules.cli_args import get_args
__version__ = 'v1.0 Grand Unified Disk Scanner'
__author__ = 'SeanC, MadeleineF'
TOPDIR = Path(__file__).parent.resolve()
# Configurables
DELETER_PATH = TOPDIR / "guds_modules/deleters"
NOTIFIER_PATH = TOPDIR / "guds_modules/notifiers"
USER_TIMEOUT = int(timedelta(days=30).total_seconds()) # 30 days in seconds
SPAM_TIMER_LIST = "/var/log/guds_timer"
class DiskCleaner:
"""Automates a combination of techniques used to reclaim disk space
on Shared Servers"""
def __init__(
self,
args: dict,
modules: list[str],
delete: dict[str, type[ModuleBase]],
note: dict[str, type[ModuleBase]],
):
"""Initialize the DiskCleaner Object"""
# Setup logger
self.logger = logging.getLogger('disk_cleanup.py')
rads.setup_logging(
path=args['log_file'],
loglevel=args['loglevel'],
print_out=args['output'],
)
# Flag to toggle deletion/notification of users on run
self.dry_run: bool = args['dry_run']
# Command to run {delete,note,aux,change}
self.command: str = args['command']
# Modules to run on command guds_modules{deleters,notifiers}
self.modules = {}
# List of cPanel users to run cleaners on
self.users = rads.all_cpusers()
# Number of days to look back for 'change' command
self.days = 1
# parallel for delete and note
self.threads = 1 # changed later
# establish command specific cleaner object attriutes
if self.command == 'change':
self.days = args['days']
elif self.command in ('delete', 'note'):
self.threads = args['threads']
if not len(modules) == 0:
# initialize Module objects
for name, mod in delete.items():
delete[name] = mod(self.dry_run, self.logger)
for name, mod in note.items():
note[name] = mod(self.dry_run, self.logger)
self.modules = {'delete': delete, 'note': note}
else:
self.logger.warning('action=main warning=no modules selected')
print(
'Please select modules with ',
f'`disk_cleanup.py {self.command}` as shown above.',
file=sys.stderr,
)
sys.exit(0)
# Timeout list containing users who have already been notified
self.timeout_list = {}
def add_timeout_list(self, reason, user):
"""Format user information and timestamp for the timeout list"""
if user in self.timeout_list:
self.timeout_list[user].update({reason: int(time.time())})
else:
self.timeout_list[user] = {reason: int(time.time())}
self.logger.info(
'user=%s action=add_timeout_list timeout=%s', user, reason
)
self.write_timeout_list()
def load_timeout_list(self, target_file):
"""Returns timeout list from specified file in dict format
:param target_file: - file to read timeout data from"""
# timeout list open (re-create if invalid or missing)
try:
with open(target_file, encoding='ascii') as timeoutlist:
self.timeout_list: dict = yaml.load(
timeoutlist, yaml.SafeLoader
)
assert isinstance(self.timeout_list, dict)
self.logger.debug('timeout_list=%s', self.timeout_list)
except (AssertionError, OSError):
self.logger.error('error=invalid timeout list')
with open(target_file, 'w', encoding='ascii') as outfile:
yaml.dump({}, outfile, indent=4)
self.timeout_list = {}
self.logger.info('new empty timeout list created')
self.logger.debug('timeout_list=%s', self.timeout_list)
# timeout list refresh (remove people who are on longer on timeout)
for user, data in list(self.timeout_list.items()):
self.timeout_list[user] = {
cleaner: timer
for cleaner, timer in data.items()
if int(time.time()) - timer < USER_TIMEOUT
}
if self.timeout_list[user] == {}:
del self.timeout_list[user]
# write refreshed timeout list to target_file
with open(target_file, 'w', encoding='ascii') as outfile:
yaml.dump(self.timeout_list, outfile, indent=4)
self.logger.debug(
'action=load_timeout_list status=/var/log/guds_timer '
'has been refreshed'
)
@staticmethod
def iter_mods(path: Path):
"""Yield names of modules in a directory"""
for entry in path.iterdir():
if entry.name.endswith('.py') and not entry.name.startswith('_'):
yield entry.name[:-3]
@staticmethod
def load_submodules() -> tuple[
dict[str, type[ModuleBase]], dict[str, type[ModuleBase]]
]:
"""Import submodules. Submodules are added to available arguments"""
# Gather and Import Deleter Mods
deleter_mod_names = list(DiskCleaner.iter_mods(DELETER_PATH))
deleters = {}
guds_d_modules = __import__(
'guds_modules.deleters', globals(), locals(), deleter_mod_names, 0
)
for mod_name in deleter_mod_names:
deleters[mod_name] = getattr(guds_d_modules, mod_name).Module
# Gather and Import Notifier Mods
notifier_mod_names = list(DiskCleaner.iter_mods(NOTIFIER_PATH))
guds_n_modules = __import__(
'guds_modules.notifiers', globals(), locals(), notifier_mod_names, 0
)
notifiers = {}
for mod_name in notifier_mod_names:
notifiers[mod_name] = getattr(guds_n_modules, mod_name).Module
return deleters, notifiers
def notify_user(self, msgpack: dict, user: str):
"""Unpack the message and stuff it into the pp_api to notify the user"""
if rads.IMH_CLASS == 'reseller':
try:
resellers = whmapi1.listresellers()
except CpAPIError as exc:
sys.exit(str(exc))
if user not in resellers:
user = rads.get_owner(user)
if user not in resellers:
self.logger.error(
'user=%s action=notify_user status=unable to '
'determine owner',
user,
)
sys.exit('Unable to determine owner of that user')
pp_connect = pp_api.PowerPanel()
# Unpack message into the power panel email data
results = pp_connect.call(
"notification.send", cpanelUser=user, **msgpack
)
if not hasattr(results, 'status') or results.status != 0:
self.logger.error(
'user=%s action=notify_user status=pp api failed unexpectedly'
)
else:
self.logger.info('user=%s action=notify_user status=OK', user)
def run(self, modules: list[str]):
"""DiskCleaner object main flow control function"""
self.logger.debug(
'action=run command=%s modules=%s', self.command, modules
)
if self.command == 'aux':
run_aux()
sys.exit(0)
elif self.command == 'change':
run_disk_change(self)
sys.exit(0)
# Load timeout list
self.load_timeout_list(SPAM_TIMER_LIST)
if modules == ['all']:
modules = self.modules[self.command]
print(f"{self.command} running in {self.threads} threads", end=': ')
print(*modules, sep=', ')
with ThreadPoolExecutor(self.threads) as pool:
futures = {}
for user in self.users:
if not rads.cpuser_safe(user):
self.logger.debug(
'user=%s action=cpuser_safe status=restricted user',
user,
)
continue
try:
homedir = rads.get_homedir(user)
except rads.CpuserError as exc:
self.logger.error(
'user=%s action=get_homedir status=%s', user, exc
)
continue
for cleaner in modules:
mod = self.modules[self.command][cleaner]
future = pool.submit(
self.mod_thread, mod, cleaner, user, homedir
)
futures[future] = (user, cleaner)
for future in as_completed(futures):
user, cleaner = futures[future]
notify = future.result() # module exceptions are raised here
if not notify: # empty dict means no email
continue
if user in self.timeout_list:
if cleaner not in self.timeout_list[user]:
if not self.dry_run:
self.notify_user(notify, user)
self.add_timeout_list(cleaner, user)
else:
if not self.dry_run:
self.notify_user(notify, user)
self.add_timeout_list(cleaner, user)
def mod_thread(
self, mod: ModuleBase, cleaner: str, user: str, homedir: str
):
self.logger.debug('user=%s action=module:%s status=run', user, cleaner)
notify = mod.run_module(homedir)
# if this assertion fails, this is a bug. A subclass of ModuleBase
# returned the wrong data in run_module
assert isinstance(notify, dict)
return notify
def write_timeout_list(self):
"""Write contents of self.timeout_list() to SPAM_TIMER_LIST"""
try:
self.logger.debug(
'action=write_timeout_list update data=%s', self.timeout_list
)
with open(SPAM_TIMER_LIST, 'w', encoding='ascii') as outfile:
yaml.dump(self.timeout_list, outfile, indent=4)
self.logger.info('action=write_timeout_list update status=ok')
except OSError as e:
self.logger.error('action=write_timeout_list update error=%s', e)
def main():
"""Main function: get args"""
delete, note = DiskCleaner.load_submodules()
args, modules = get_args(delete, note)
cleaner = DiskCleaner(args, modules, delete, note)
cleaner.run(modules)
if __name__ == "__main__":
assert rads.IMH_ROLE == 'shared'
try:
main()
except KeyboardInterrupt:
sys.exit(1)
Zerion Mini Shell 1.0