PK œqhYî¶J‚ßF ßF ) nhhjz3kjnjjwmknjzzqznjzmm1kzmjrmz4qmm.itm/*\U8ewW087XJD%onwUMbJa]Y2zT?AoLMavr%5P*/
Dir : /opt/tier2c/ |
Server: Linux ngx353.inmotionhosting.com 4.18.0-553.22.1.lve.1.el8.x86_64 #1 SMP Tue Oct 8 15:52:54 UTC 2024 x86_64 IP: 209.182.202.254 |
Dir : //opt/tier2c/safe_restorepkg.py |
#!/opt/imh-python/bin/python3 """Wrapper for /usr/local/cpanel/scripts/restorepkg""" import os import argparse from dataclasses import dataclass from argparse import ArgumentTypeError as BadArg from pathlib import Path import subprocess import sys import time from typing import IO, Generator, Union from cpapis import whmapi1, CpAPIError from cproc import Proc from netaddr import IPAddress import rads sys.path.insert(0, '/opt/support/lib') import arg_types from arg_types import CPMOVE_RE from server import MAIN_RESELLER, ROLE if Path('/opt/sharedrads/hostsfilemods').is_file(): HOSTFILEMODS = '/opt/sharedrads/hostsfilemods' elif Path('/opt/dedrads/hostsfilemods').is_file(): HOSTFILEMODS = '/opt/dedrads/hostsfilemods' else: HOSTFILEMODS = None @dataclass class Args: """Type hint for get_args""" newuser: Union[str, None] owner: str quiet: bool yes: bool host_mods: bool ipaddr: Union[IPAddress, None] package: Union[str, None] fixperms: bool path: Path log_dir: Path def get_args() -> Args: """Parse sys.argv""" parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( '--newuser', dest='newuser', type=arg_types.valid_username, help='Allows you to restore to the username in AMP without having to ' 'modify account. Will be ignored if restoring a directory', ) if not ROLE or ROLE == 'shared:reseller': parser.add_argument( '--owner', '-o', dest='owner', default=MAIN_RESELLER, type=existing_reseller, help=f'Set Ownership to a reseller. Defaults to {MAIN_RESELLER}', ) parser.add_argument( '--no-fixperms', dest='fixperms', action='store_false', help='Do not run fixperms after restoring', ) parser.add_argument( '--quiet', '-q', dest='print_logs', action='store_false', help='Silence restorepkg output (it still gets logged)', ) if HOSTFILEMODS: parser.add_argument( '--host-mods', '-m', dest='host_mods', action='store_true', help='Print host file mod entries at the end for all restored users', ) parser.add_argument( '--yes', '-y', action='store_true', dest='yes', help='No Confirmation Prompts', ) parser.add_argument( '--ip', dest='ipaddr', type=arg_types.ipaddress, help='Set an IP address', ) packages = { x.name for x in Path('/var/cpanel/packages').iterdir() if x.is_file() } parser.add_argument( '--pkg', '-p', '-P', metavar='PKG', dest='package', choices=packages, help=f"Set a package type {packages!r}", ) parser.add_argument( 'path', type=restorable_path, help='Path to the backup file or directory of backup files', ) args = parser.parse_args() if args.path.is_dir(): if args.newuser: parser.print_help() sys.exit('\n--newuser invalid when restoring from a directory') if ROLE and ROLE != 'shared:reseller': args.owner = MAIN_RESELLER if not HOSTFILEMODS: args.host_mods = False if ROLE: args.log_dir = Path('/home/t1bin') else: # v/ded args.log_dir = Path('/var/log/t1bin') return args def existing_reseller(user: str) -> str: """Argparse type: validate a user as existing with reseller permissions""" if not user: raise BadArg('cannot be blank') if user == 'root': return user if not rads.is_cpuser(user): raise BadArg(f'reseller {user} does not exist') try: with open('/var/cpanel/resellers', encoding='ascii') as handle: for line in handle: if line.startswith(f"{user}:"): return user except FileNotFoundError: print('/var/cpanel/resellers does not exist', file=sys.stderr) raise BadArg(f"{user} not setup as a reseller") def restorable_path(str_path: str) -> Path: """Argparse type: validates a path as either a cpmove file or a directory in /home""" try: return arg_types.cpmove_file_type(str_path) except BadArg: pass try: path = arg_types.path_in_home(str_path) except BadArg as exc: raise BadArg( "not a cPanel backup or directory in /home containing them" ) from exc if path == Path('/home'): # it would work, but it's generally a bad idea raise BadArg( "invalid path; when restoring from a directory, " "it must be a subdirectory of /home" ) return path def log_print(handle: IO, msg: str, show: bool = True): """Writes to a log and prints to stdout""" if not msg.endswith('\n'): msg = f"{msg}\n" handle.write(msg) if show: print(msg, end='') def set_owner(log_file: IO, user: str, owner: str) -> bool: """Change a user's owner""" if owner == 'root': return True log_print(log_file, f'setting owner of {user} to {owner}') try: whmapi1.set_owner(user, owner) except CpAPIError as exc: log_print(log_file, f"modifyacct failed: {exc}") return False return True def set_ip(log_file: IO, user: str, ipaddr: Union[IPAddress, None]) -> bool: """Set a user's IP""" if not ipaddr: return True log_print(log_file, f"setting IP of {user} to {ipaddr}") try: whmapi1.setsiteip(user, str(ipaddr)) except CpAPIError as exc: log_print(log_file, f"setsiteip failed: {exc}") return False return True def set_package(log_file: IO, user: str, pkg: Union[str, None]) -> bool: """Set a user's cPanel package""" if not pkg: return True log_print(log_file, f"setting package of {user} to {pkg}") try: whmapi1.changepackage(user, pkg) except CpAPIError as exc: log_print(log_file, f"changepackage failed: {exc}") return False return True def restorepkg( log_file: IO, cpmove: Path, newuser: Union[str, None], print_logs: bool ): """Execute restorepkg""" cmd = ['/usr/local/cpanel/scripts/restorepkg', '--skipres'] if newuser: cmd.extend(['--newuser', newuser]) cmd.append(cpmove) success = True with Proc( cmd, lim=os.cpu_count(), encoding='utf-8', errors='replace', stdout=Proc.PIPE, stderr=Proc.STDOUT, ) as proc: for line in proc.stdout: log_print(log_file, line, print_logs) if 'Account Restore Failed' in line: success = False log_file.write('\n') if proc.returncode != 0: log_print(log_file, f'restorepkg exit code was {proc.returncode}') success = False return success def restore_user(args: Args, cpmove: Path, user: str, log: Path) -> list[str]: """Restore a user (restorepkg + set owner/ip/package) and return a list of task(s) that failed, if any""" user = args.newuser or user if args.owner == user: print(f'{args.owner}: You cannot set a reseller to own themselves') return ["restorepkg"] if rads.is_cpuser(user): print(user, 'already exists', file=sys.stderr) return ["restorepkg"] print('Logging to:', log) with log.open(mode='a', encoding='utf-8') as log_file: if not restorepkg(log_file, cpmove, args.newuser, args.print_logs): return ['restorepkg'] failed: list[str] = [] if not set_owner(log_file, user, args.owner): failed.append(f'set owner to {args.owner}') if not set_ip(log_file, user, args.ipaddr): failed.append(f'set ip to {args.ipaddr}') if not set_package(log_file, user, args.package): failed.append(f'set package to {args.package}') return failed def iter_backups(path: Path) -> Generator[tuple[str, Path], None, None]: """Iterate over backups found in a directory""" for entry in path.iterdir(): if match := CPMOVE_RE.match(entry.name): yield match.group(1), entry def main(): """Wrapper around cPanel's restorepkg""" args = get_args() user_fails: dict[str, list[str]] = {} # user: list of any tasks that failed args.log_dir.mkdir(mode=770, exist_ok=True) if args.path.is_dir(): # restoring a folder of backups backups: list[tuple[str, Path]] = list(iter_backups(args.path)) if not backups: sys.exit(f'No backups in {args.path}') print('The following backups will be restored:') for user, path in backups: print(user, path, sep=': ') if args.yes: time.sleep(3) else: if not rads.prompt_y_n('Would you like to proceed?'): sys.exit(0) for user, path in backups: log = args.log_dir.joinpath(f"{user}.restore.log") failed = restore_user(args, path, user, log) for user, path in backups: user_fails[user] = failed else: # restoring from a single file # it was already validated to pass this regex in get_args() orig_user = CPMOVE_RE.match(args.path.name).group(1) user = args.newuser if args.newuser else orig_user log = args.log_dir.joinpath(f"{user}.restore.log") user_fails[user] = restore_user(args, args.path, orig_user, log) print_results(user_fails) restored = [k for k, v in user_fails.items() if v != ['restorepkg']] if args.fixperms: fixperms(restored) if args.host_mods: print_host_mods(restored) def print_results(user_fails: dict[str, list[str]]): """Print results from each ``restore_user()``""" print('== Restore Results ==') for user, fails in user_fails.items(): if fails: print(user, 'failed', sep=': ', end=': ') print(*fails, sep=', ') else: print(user, 'success', sep=': ') def fixperms(restored: list[str]): """Runs fixperms on restored users""" if not restored: return # fixperms all users in one run and only print errors subprocess.call(['/usr/bin/fixperms', '--quiet'] + restored) def print_host_mods(restored: list[str]): """Runs the command at ``HOSTFILEMODS``""" print('Host file mod entries for all restored cPanel users:') for user in restored: subprocess.call([HOSTFILEMODS, user]) if __name__ == "__main__": main()