Mini Shell

Direktori : /opt/sharedrads/
Upload File :
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