Mini Shell
Direktori : /opt/tier2c/ |
|
Current File : //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()
Zerion Mini Shell 1.0