Edit File by line
/home/barbar84/public_h.../wp-conte.../plugins/sujqvwi/AnonR/smanonr..../opt/sharedra...
File: disk_cleanup.py
#!/opt/imh-python/bin/python3
[0] Fix | Delete
"""Grand Unified Disk Scanner.
[1] Fix | Delete
[2] Fix | Delete
For More Information and Usage:
[3] Fix | Delete
http://wiki.inmotionhosting.com/index.php?title=RADS#disk_cleanup.py"""
[4] Fix | Delete
[5] Fix | Delete
[6] Fix | Delete
import logging
[7] Fix | Delete
import sys
[8] Fix | Delete
from datetime import timedelta
[9] Fix | Delete
from pathlib import Path
[10] Fix | Delete
import time
[11] Fix | Delete
from concurrent.futures import ThreadPoolExecutor, as_completed
[12] Fix | Delete
import pp_api
[13] Fix | Delete
import yaml
[14] Fix | Delete
from cpapis import whmapi1, CpAPIError
[15] Fix | Delete
import rads
[16] Fix | Delete
from guds_modules.change import run_disk_change
[17] Fix | Delete
from guds_modules.aux import run_aux
[18] Fix | Delete
from guds_modules.base import ModuleBase
[19] Fix | Delete
from guds_modules.cli_args import get_args
[20] Fix | Delete
[21] Fix | Delete
__version__ = 'v1.0 Grand Unified Disk Scanner'
[22] Fix | Delete
__author__ = 'SeanC, MadeleineF'
[23] Fix | Delete
[24] Fix | Delete
TOPDIR = Path(__file__).parent.resolve()
[25] Fix | Delete
[26] Fix | Delete
# Configurables
[27] Fix | Delete
DELETER_PATH = TOPDIR / "guds_modules/deleters"
[28] Fix | Delete
NOTIFIER_PATH = TOPDIR / "guds_modules/notifiers"
[29] Fix | Delete
USER_TIMEOUT = int(timedelta(days=30).total_seconds()) # 30 days in seconds
[30] Fix | Delete
SPAM_TIMER_LIST = "/var/log/guds_timer"
[31] Fix | Delete
[32] Fix | Delete
[33] Fix | Delete
class DiskCleaner:
[34] Fix | Delete
"""Automates a combination of techniques used to reclaim disk space
[35] Fix | Delete
on Shared Servers"""
[36] Fix | Delete
[37] Fix | Delete
def __init__(
[38] Fix | Delete
self,
[39] Fix | Delete
args: dict,
[40] Fix | Delete
modules: list[str],
[41] Fix | Delete
delete: dict[str, type[ModuleBase]],
[42] Fix | Delete
note: dict[str, type[ModuleBase]],
[43] Fix | Delete
):
[44] Fix | Delete
"""Initialize the DiskCleaner Object"""
[45] Fix | Delete
# Setup logger
[46] Fix | Delete
self.logger = logging.getLogger('disk_cleanup.py')
[47] Fix | Delete
rads.setup_logging(
[48] Fix | Delete
path=args['log_file'],
[49] Fix | Delete
loglevel=args['loglevel'],
[50] Fix | Delete
print_out=args['output'],
[51] Fix | Delete
)
[52] Fix | Delete
# Flag to toggle deletion/notification of users on run
[53] Fix | Delete
self.dry_run: bool = args['dry_run']
[54] Fix | Delete
# Command to run {delete,note,aux,change}
[55] Fix | Delete
self.command: str = args['command']
[56] Fix | Delete
# Modules to run on command guds_modules{deleters,notifiers}
[57] Fix | Delete
self.modules = {}
[58] Fix | Delete
# List of cPanel users to run cleaners on
[59] Fix | Delete
self.users = rads.all_cpusers()
[60] Fix | Delete
# Number of days to look back for 'change' command
[61] Fix | Delete
self.days = 1
[62] Fix | Delete
# parallel for delete and note
[63] Fix | Delete
self.threads = 1 # changed later
[64] Fix | Delete
# establish command specific cleaner object attriutes
[65] Fix | Delete
if self.command == 'change':
[66] Fix | Delete
self.days = args['days']
[67] Fix | Delete
elif self.command in ('delete', 'note'):
[68] Fix | Delete
self.threads = args['threads']
[69] Fix | Delete
if not len(modules) == 0:
[70] Fix | Delete
# initialize Module objects
[71] Fix | Delete
for name, mod in delete.items():
[72] Fix | Delete
delete[name] = mod(self.dry_run, self.logger)
[73] Fix | Delete
for name, mod in note.items():
[74] Fix | Delete
note[name] = mod(self.dry_run, self.logger)
[75] Fix | Delete
self.modules = {'delete': delete, 'note': note}
[76] Fix | Delete
else:
[77] Fix | Delete
self.logger.warning('action=main warning=no modules selected')
[78] Fix | Delete
print(
[79] Fix | Delete
'Please select modules with ',
[80] Fix | Delete
f'`disk_cleanup.py {self.command}` as shown above.',
[81] Fix | Delete
file=sys.stderr,
[82] Fix | Delete
)
[83] Fix | Delete
sys.exit(0)
[84] Fix | Delete
# Timeout list containing users who have already been notified
[85] Fix | Delete
self.timeout_list = {}
[86] Fix | Delete
[87] Fix | Delete
def add_timeout_list(self, reason, user):
[88] Fix | Delete
"""Format user information and timestamp for the timeout list"""
[89] Fix | Delete
if user in self.timeout_list:
[90] Fix | Delete
self.timeout_list[user].update({reason: int(time.time())})
[91] Fix | Delete
else:
[92] Fix | Delete
self.timeout_list[user] = {reason: int(time.time())}
[93] Fix | Delete
self.logger.info(
[94] Fix | Delete
'user=%s action=add_timeout_list timeout=%s', user, reason
[95] Fix | Delete
)
[96] Fix | Delete
self.write_timeout_list()
[97] Fix | Delete
[98] Fix | Delete
def load_timeout_list(self, target_file):
[99] Fix | Delete
"""Returns timeout list from specified file in dict format
[100] Fix | Delete
:param target_file: - file to read timeout data from"""
[101] Fix | Delete
# timeout list open (re-create if invalid or missing)
[102] Fix | Delete
try:
[103] Fix | Delete
with open(target_file, encoding='ascii') as timeoutlist:
[104] Fix | Delete
self.timeout_list: dict = yaml.load(
[105] Fix | Delete
timeoutlist, yaml.SafeLoader
[106] Fix | Delete
)
[107] Fix | Delete
assert isinstance(self.timeout_list, dict)
[108] Fix | Delete
self.logger.debug('timeout_list=%s', self.timeout_list)
[109] Fix | Delete
except (AssertionError, OSError):
[110] Fix | Delete
self.logger.error('error=invalid timeout list')
[111] Fix | Delete
with open(target_file, 'w', encoding='ascii') as outfile:
[112] Fix | Delete
yaml.dump({}, outfile, indent=4)
[113] Fix | Delete
self.timeout_list = {}
[114] Fix | Delete
self.logger.info('new empty timeout list created')
[115] Fix | Delete
self.logger.debug('timeout_list=%s', self.timeout_list)
[116] Fix | Delete
[117] Fix | Delete
# timeout list refresh (remove people who are on longer on timeout)
[118] Fix | Delete
for user, data in list(self.timeout_list.items()):
[119] Fix | Delete
self.timeout_list[user] = {
[120] Fix | Delete
cleaner: timer
[121] Fix | Delete
for cleaner, timer in data.items()
[122] Fix | Delete
if int(time.time()) - timer < USER_TIMEOUT
[123] Fix | Delete
}
[124] Fix | Delete
if self.timeout_list[user] == {}:
[125] Fix | Delete
del self.timeout_list[user]
[126] Fix | Delete
[127] Fix | Delete
# write refreshed timeout list to target_file
[128] Fix | Delete
with open(target_file, 'w', encoding='ascii') as outfile:
[129] Fix | Delete
yaml.dump(self.timeout_list, outfile, indent=4)
[130] Fix | Delete
self.logger.debug(
[131] Fix | Delete
'action=load_timeout_list status=/var/log/guds_timer '
[132] Fix | Delete
'has been refreshed'
[133] Fix | Delete
)
[134] Fix | Delete
[135] Fix | Delete
@staticmethod
[136] Fix | Delete
def iter_mods(path: Path):
[137] Fix | Delete
"""Yield names of modules in a directory"""
[138] Fix | Delete
for entry in path.iterdir():
[139] Fix | Delete
if entry.name.endswith('.py') and not entry.name.startswith('_'):
[140] Fix | Delete
yield entry.name[:-3]
[141] Fix | Delete
[142] Fix | Delete
@staticmethod
[143] Fix | Delete
def load_submodules() -> tuple[
[144] Fix | Delete
dict[str, type[ModuleBase]], dict[str, type[ModuleBase]]
[145] Fix | Delete
]:
[146] Fix | Delete
"""Import submodules. Submodules are added to available arguments"""
[147] Fix | Delete
# Gather and Import Deleter Mods
[148] Fix | Delete
deleter_mod_names = list(DiskCleaner.iter_mods(DELETER_PATH))
[149] Fix | Delete
deleters = {}
[150] Fix | Delete
guds_d_modules = __import__(
[151] Fix | Delete
'guds_modules.deleters', globals(), locals(), deleter_mod_names, 0
[152] Fix | Delete
)
[153] Fix | Delete
for mod_name in deleter_mod_names:
[154] Fix | Delete
deleters[mod_name] = getattr(guds_d_modules, mod_name).Module
[155] Fix | Delete
[156] Fix | Delete
# Gather and Import Notifier Mods
[157] Fix | Delete
notifier_mod_names = list(DiskCleaner.iter_mods(NOTIFIER_PATH))
[158] Fix | Delete
guds_n_modules = __import__(
[159] Fix | Delete
'guds_modules.notifiers', globals(), locals(), notifier_mod_names, 0
[160] Fix | Delete
)
[161] Fix | Delete
notifiers = {}
[162] Fix | Delete
for mod_name in notifier_mod_names:
[163] Fix | Delete
notifiers[mod_name] = getattr(guds_n_modules, mod_name).Module
[164] Fix | Delete
[165] Fix | Delete
return deleters, notifiers
[166] Fix | Delete
[167] Fix | Delete
def notify_user(self, msgpack: dict, user: str):
[168] Fix | Delete
"""Unpack the message and stuff it into the pp_api to notify the user"""
[169] Fix | Delete
if rads.IMH_CLASS == 'reseller':
[170] Fix | Delete
try:
[171] Fix | Delete
resellers = whmapi1.listresellers()
[172] Fix | Delete
except CpAPIError as exc:
[173] Fix | Delete
sys.exit(str(exc))
[174] Fix | Delete
if user not in resellers:
[175] Fix | Delete
user = rads.get_owner(user)
[176] Fix | Delete
if user not in resellers:
[177] Fix | Delete
self.logger.error(
[178] Fix | Delete
'user=%s action=notify_user status=unable to '
[179] Fix | Delete
'determine owner',
[180] Fix | Delete
user,
[181] Fix | Delete
)
[182] Fix | Delete
sys.exit('Unable to determine owner of that user')
[183] Fix | Delete
[184] Fix | Delete
pp_connect = pp_api.PowerPanel()
[185] Fix | Delete
[186] Fix | Delete
# Unpack message into the power panel email data
[187] Fix | Delete
results = pp_connect.call(
[188] Fix | Delete
"notification.send", cpanelUser=user, **msgpack
[189] Fix | Delete
)
[190] Fix | Delete
if not hasattr(results, 'status') or results.status != 0:
[191] Fix | Delete
self.logger.error(
[192] Fix | Delete
'user=%s action=notify_user status=pp api failed unexpectedly'
[193] Fix | Delete
)
[194] Fix | Delete
else:
[195] Fix | Delete
self.logger.info('user=%s action=notify_user status=OK', user)
[196] Fix | Delete
[197] Fix | Delete
def run(self, modules: list[str]):
[198] Fix | Delete
"""DiskCleaner object main flow control function"""
[199] Fix | Delete
self.logger.debug(
[200] Fix | Delete
'action=run command=%s modules=%s', self.command, modules
[201] Fix | Delete
)
[202] Fix | Delete
if self.command == 'aux':
[203] Fix | Delete
run_aux()
[204] Fix | Delete
sys.exit(0)
[205] Fix | Delete
elif self.command == 'change':
[206] Fix | Delete
run_disk_change(self)
[207] Fix | Delete
sys.exit(0)
[208] Fix | Delete
[209] Fix | Delete
# Load timeout list
[210] Fix | Delete
self.load_timeout_list(SPAM_TIMER_LIST)
[211] Fix | Delete
[212] Fix | Delete
if modules == ['all']:
[213] Fix | Delete
modules = self.modules[self.command]
[214] Fix | Delete
print(f"{self.command} running in {self.threads} threads", end=': ')
[215] Fix | Delete
print(*modules, sep=', ')
[216] Fix | Delete
[217] Fix | Delete
with ThreadPoolExecutor(self.threads) as pool:
[218] Fix | Delete
futures = {}
[219] Fix | Delete
for user in self.users:
[220] Fix | Delete
if not rads.cpuser_safe(user):
[221] Fix | Delete
self.logger.debug(
[222] Fix | Delete
'user=%s action=cpuser_safe status=restricted user',
[223] Fix | Delete
user,
[224] Fix | Delete
)
[225] Fix | Delete
continue
[226] Fix | Delete
try:
[227] Fix | Delete
homedir = rads.get_homedir(user)
[228] Fix | Delete
except rads.CpuserError as exc:
[229] Fix | Delete
self.logger.error(
[230] Fix | Delete
'user=%s action=get_homedir status=%s', user, exc
[231] Fix | Delete
)
[232] Fix | Delete
continue
[233] Fix | Delete
for cleaner in modules:
[234] Fix | Delete
mod = self.modules[self.command][cleaner]
[235] Fix | Delete
future = pool.submit(
[236] Fix | Delete
self.mod_thread, mod, cleaner, user, homedir
[237] Fix | Delete
)
[238] Fix | Delete
futures[future] = (user, cleaner)
[239] Fix | Delete
for future in as_completed(futures):
[240] Fix | Delete
user, cleaner = futures[future]
[241] Fix | Delete
notify = future.result() # module exceptions are raised here
[242] Fix | Delete
if not notify: # empty dict means no email
[243] Fix | Delete
continue
[244] Fix | Delete
if user in self.timeout_list:
[245] Fix | Delete
if cleaner not in self.timeout_list[user]:
[246] Fix | Delete
if not self.dry_run:
[247] Fix | Delete
self.notify_user(notify, user)
[248] Fix | Delete
self.add_timeout_list(cleaner, user)
[249] Fix | Delete
else:
[250] Fix | Delete
if not self.dry_run:
[251] Fix | Delete
self.notify_user(notify, user)
[252] Fix | Delete
self.add_timeout_list(cleaner, user)
[253] Fix | Delete
[254] Fix | Delete
def mod_thread(
[255] Fix | Delete
self, mod: ModuleBase, cleaner: str, user: str, homedir: str
[256] Fix | Delete
):
[257] Fix | Delete
self.logger.debug('user=%s action=module:%s status=run', user, cleaner)
[258] Fix | Delete
notify = mod.run_module(homedir)
[259] Fix | Delete
# if this assertion fails, this is a bug. A subclass of ModuleBase
[260] Fix | Delete
# returned the wrong data in run_module
[261] Fix | Delete
assert isinstance(notify, dict)
[262] Fix | Delete
return notify
[263] Fix | Delete
[264] Fix | Delete
def write_timeout_list(self):
[265] Fix | Delete
"""Write contents of self.timeout_list() to SPAM_TIMER_LIST"""
[266] Fix | Delete
try:
[267] Fix | Delete
self.logger.debug(
[268] Fix | Delete
'action=write_timeout_list update data=%s', self.timeout_list
[269] Fix | Delete
)
[270] Fix | Delete
with open(SPAM_TIMER_LIST, 'w', encoding='ascii') as outfile:
[271] Fix | Delete
yaml.dump(self.timeout_list, outfile, indent=4)
[272] Fix | Delete
self.logger.info('action=write_timeout_list update status=ok')
[273] Fix | Delete
except OSError as e:
[274] Fix | Delete
self.logger.error('action=write_timeout_list update error=%s', e)
[275] Fix | Delete
[276] Fix | Delete
[277] Fix | Delete
def main():
[278] Fix | Delete
"""Main function: get args"""
[279] Fix | Delete
delete, note = DiskCleaner.load_submodules()
[280] Fix | Delete
args, modules = get_args(delete, note)
[281] Fix | Delete
cleaner = DiskCleaner(args, modules, delete, note)
[282] Fix | Delete
cleaner.run(modules)
[283] Fix | Delete
[284] Fix | Delete
[285] Fix | Delete
if __name__ == "__main__":
[286] Fix | Delete
assert rads.IMH_ROLE == 'shared'
[287] Fix | Delete
try:
[288] Fix | Delete
main()
[289] Fix | Delete
except KeyboardInterrupt:
[290] Fix | Delete
sys.exit(1)
[291] Fix | Delete
[292] Fix | Delete
It is recommended that you Edit text format, this type of Fix handles quite a lot in one request
Function