Mini Shell

Direktori : /opt/sharedrads/
Upload File :
Current File : //opt/sharedrads/remote_dump

#!/opt/imh-python/bin/python3
__author__ = 'chases'
import os
import shlex
import socket
import argparse
import sys
import base64
import subprocess
import threading
from typing import IO
from time import sleep
from urllib.parse import urlencode
import requests
from requests.adapters import TimeoutSauce
from prettytable import PrettyTable
from rads import prompt_y_n, is_cpuser, SECURE_USER, SYS_USERS, get_owner
from rads.color import red, yellow, green
from cpapis import whmapi1, CpAPIError


def check_user(username):
    """Checks whether the user is valid for switching.
    Taken from /opt/tier1adv/bin/switch"""
    restricted_users = SYS_USERS
    sec_user = SECURE_USER
    if sec_user is not None:  # will be None on VPS/Ded
        restricted_users.append(sec_user)

    if username in restricted_users:
        print(red(f"You may not remove {username}."))
        return False

    if not is_cpuser(username):
        print(f"The user {username} is not a cpanel user.")
        return False

    return True


def input_parse():
    parser = argparse.ArgumentParser()
    # fmt: off
    parser.add_argument(
        '-u', '--user', action='store', dest='user',
        help='Define your remote cpanel user or reseller',
    )
    parser.add_argument(
        '-d', '--domain', action='store', dest='domain', default=False,
        help='set domain if cannot be pulled from listaccount (no reseller)',
    )
    parser.add_argument(
        '-p', '--pass', action='store', dest='res_pass', default=None,
        help='Set the password for the remote reseller or cpuser. Not '
        'recommended to pass as an argument, but an option nonetheless.',
    )
    parser.add_argument(
        '--replace', action='store', dest='replace', default=False,
        help='Replace a cpanel user with the backup restored.',
    )
    parser.add_argument(
        '-r', '--host', action='store', dest='remote_host', default=None,
        help='Remote Hostname.',
    )
    parser.add_argument(
        '-n', '--nosize', action='store_true', default=False, dest='nosize',
        help='Ignore size limitations',
    )
    parser.add_argument(
        '--norestore', action='store_true', default=False, dest='norestore',
        help='Will not restore the package',
    )
    parser.add_argument(
        '--skip', action='store', default=False, nargs='+', dest='skip',
        help='Skip specific accounts. Useful with --all',
    )
    parser.add_argument(
        '-i', '--ip', action='store', dest='IP', default=False,
        help='Set the IP address on Restore.',
    )
    parser.add_argument(
        '-o', '--owner', action='store', dest='owner', default=False,
        help='Set the Owner on restore',
    )
    parser.add_argument(
        '-P', '--package', action='store', dest='package', default=False,
        help='Set the package on restore',
    )
    parser.add_argument(
        '--all', action='store_true', default=False, dest='all',
        help='Dump all backups for accounts within size specification',
    )
    parser.add_argument('--version', action='version', version='%(prog)s 2.0')
    # fmt: on
    raw = parser.parse_known_args()
    parsed = raw[0]
    try:
        if not parsed.user:
            parsed.user = input(
                "Enter the reseller user from the remote host: "
            )
        if not parsed.res_pass:
            parsed.res_pass = input(
                "Enter the reseller password for the remote host: "
            )
        if not parsed.remote_host:
            parsed.remote_host = input("Enter remote hostname or IP address: ")
        if parsed.replace:
            if os.path.isfile(f'/var/cpanel/users/{parsed.replace}'):
                choice = prompt_y_n(
                    f'Are you sure you would like to replace {parsed.replace} '
                    f'which is owned by: {get_owner(parsed.replace)}'
                )
                if not choice:
                    parsed.replace = False
            else:
                parsed.replace = False
    except KeyboardInterrupt:
        sys.exit('\nQuitting')
    return parsed


class MyTimeout(TimeoutSauce):
    def __init__(self, *_args, **kwargs):
        connect = kwargs.get('connect', 5)
        read = kwargs.get('read', connect)
        super().__init__(connect=connect, read=read)


def validate_whm_connection(parsed):
    print(
        'Trying to make a socket connection to WHM. If you know this will ',
        'not work you can Ctrl ^C to move on',
    )
    sock = socket.socket()
    address = parsed.remote_host
    port = 2086  # port number is a number, not string
    try:
        sock.connect((address, port))
    except KeyboardInterrupt:
        print('Moving on')
        return False
    except Exception as exc:
        print(exc, file=sys.stderr)
        print(red(f"Unable to connect to {address} on port {port}."))
        return False
    return True


def remote_whm_post(parsed, jsoncall, url_args, **kwargs):
    user = kwargs.get('user', parsed.user)
    port = kwargs.get('port', '2087')
    remote_host = parsed.remote_host
    if url_args:
        jsoncall += '?' + urlencode(url_args)
    url = 'https://' + remote_host + ':' + port + '/' + jsoncall
    passholder = parsed.res_pass
    encoded_pass = base64.b64encode(user + ':' + passholder)
    auth = "Basic " + encoded_pass
    header = {'Authorization': auth}
    requests.adapters.TimeoutSauce = MyTimeout
    try:
        response = requests.post(url, headers=header, verify=False, timeout=5)
    except requests.exceptions.ConnectTimeout:
        return False
    try:
        response = response.json()
    except ValueError:
        print('Unable to get valid response from', remote_host)
        print('Response received from ', remote_host, ':', response)
        return False
    return response


def _write_stdin(stdin: IO, data: str):
    stdin.write(data)
    stdin.close()


def get_cpanel_backup(
    parsed, restoreuser=False, restoredomain=False, attempt=0
):
    if restoreuser:
        parsed.user = restoreuser
    if restoredomain:
        parsed.domain = restoredomain
    restorelog = f'/tmp/parsed.{parsed.user}.backuplog'
    print(
        "Backup will start in the background. You can monitor it by tail -f",
        restorelog,
    )
    cmd = [
        '/scripts/getremotecpmove',
        parsed.remote_host,
        parsed.user,
        parsed.domain,
    ]
    with open(restorelog, 'w', encoding='utf-8') as writelog:
        with subprocess.Popen(
            cmd,
            encoding='utf-8',
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            stdin=subprocess.PIPE,
        ) as proc:
            # write to stdin in a thread so this won't deadlock if the proc
            # writes > 1024 bytes to stdout/err before consuming stdin
            thread = threading.Thread(
                target=_write_stdin,
                args=(proc.stdin, parsed.res_pass),
                daemon=True,
            )
            for line in proc.stdout:
                print(line.strip(), flush=True)
                writelog.write(line)
            thread.join()
        if proc.wait():  # non-zero exit code
            print(red('Failed to get backup from remote host.'))
            if attempt < 2:
                print(
                    red('Waiting 10 minutes and attempting again.'),
                    red('Press ctrl ^c to Manually Fail this user.'),
                    flush=True,
                )
                try:
                    sleep(600)
                except KeyboardInterrupt:
                    # This user Failed, but continuing on
                    return False
                attempt += 1
                get_cpanel_backup(parsed, attempt=attempt)
            else:
                print(red('Too many attempts failed. Moving on to next user.'))
                return False
    with open(restorelog, encoding='utf-8') as resultfile:
        for line in resultfile:
            if line.startswith('pkgacctfile is'):
                print('Backup Found: {}'.format(line.split(' ')[2].strip()))
                return line.split(' ')[2].strip()
            if 'Failed to fetch cpmove file via cPanel XML-API' in line:
                return False
    return False


def set_backup_to_standard_naming(backup, user):
    if os.path.isfile(backup):
        os.rename(backup, f'/home/cpmove-{user}.tar.gz')
        return f'/home/cpmove-{user}.tar.gz'
    return False


def restore_backup(backup, user, parsed):
    # TODO: this script is python. restorepkg is python. Import and inspect the
    # result directly rather than executing and reading its output str.
    # Do this after merging the rpms.
    cmd = ['/opt/tier2c/safe_restorepkg.py']
    if parsed.IP:
        cmd.extend(['--ip', parsed.IP.strip()])
    cmd.extend('--quiet', backup)
    status = 'Success'
    with subprocess.Popen(
        cmd,
        encoding='utf-8',
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
    ) as proc:
        for line in proc.stdout:
            print(line.strip())
            if 'Failed' in line:
                status = 'Failed'
            if line.startswith("{'%s'" % user):
                try:
                    status = line.split("'")[3]
                except KeyError:
                    pass
    if proc.wait():
        status = 'Non-Zero Exit'
    if status == 'Success' and parsed.owner:
        try:
            whmapi1.set_owner(user, parsed.owner)
        except CpAPIError as exc:
            status = str(exc)
    return status


def main():
    parsed = input_parse()
    if parsed.domain or parsed.replace or not validate_whm_connection(parsed):
        if not parsed.domain:
            print('Additional Information Required')
            parsed.domain = input(
                "Enter the primary domain for the cPanel you are transferring: "
            )
        print('Running:')
        cmd = [
            '/scripts/getremotecpmove',
            parsed.remote_host,
            parsed.user,
            parsed.domain,
        ]
        print('echo', shlex.quote(parsed.res_pass), '|', shlex.join(cmd))
        backup = get_cpanel_backup(parsed)
        if backup:
            backup = set_backup_to_standard_naming(backup, parsed.user)
            if (
                parsed.replace
                and check_user(parsed.replace)
                and backup is not False
            ):
                print(
                    yellow(f'Removing Account {parsed.replace} so it can '),
                    yellow('be restored from the backup'),
                )
                subprocess.check_call(
                    ['/scripts/removeacct', '--force', parsed.user]
                )
            if not parsed.norestore:
                restore_backup(backup, parsed.user, parsed)
    else:
        parsed.userinfo = remote_whm_post(
            parsed,
            'json-api/listaccts',
            {
                'api.version': 1,
                'want': 'user,disklimit,diskused,domain,suspended',
            },
        )
        # Check to see if the WHM connection failed and if so switch
        # to single transfer mode
        if parsed.userinfo:
            try:
                print(parsed.userinfo['cpanelresult']['data']['reason'])
                # Check for a read-only filesystem, and exit if found
                if "Read-only" in parsed.userinfo['metadata']['reason']:
                    sys.exit('Read-only file system found')

                if prompt_y_n(
                    'Connection to remote WHM Failed. Would you like to try '
                    'connecting via cPanel? Select Yes if not a reseller.'
                ):
                    if not parsed.domain:
                        print('Additional Information Required')
                        parsed.domain = input(
                            "Enter the primary domain for the cPanel "
                            "you are transferring: "
                        )
                    backup = get_cpanel_backup(parsed)
                    if backup:
                        backup = set_backup_to_standard_naming(
                            backup, parsed.user
                        )
                        if not parsed.norestore:
                            restore_backup(backup, parsed.user, parsed)
            except Exception as exc:
                print(exc, file=sys.stderr)

        # This handles the case of remote_whm_post returning False, and was
        # otherwise not a caught exception prior
        else:
            sys.exit(
                'There was an issue connecting to the remote host. Exiting.'
            )

        userdata = {}
        pickerlist = []
        toobig = []
        try:
            for userinfo in parsed.userinfo['data']['acct']:
                userdata[userinfo['user']] = {}
                userdata[userinfo['user']]['domain'] = userinfo['domain']
                userdata[userinfo['user']]['diskused'] = userinfo['diskused']
                userdata[userinfo['user']]['disklimit'] = userinfo['disklimit']
                userdata[userinfo['user']]['disklimit'] = userinfo['suspended']
                if (
                    str(userinfo['diskused'].split('M')).lower() == 'none'
                    or parsed.nosize
                    or int(userinfo['diskused'].split('M')[0]) <= 6000
                ):
                    pickerlist.append(
                        '{} - {} - {}'.format(
                            userinfo['user'],
                            userinfo['domain'],
                            userinfo['diskused'],
                        )[:50]
                    )
                if (
                    not parsed.nosize
                    and int(userinfo['diskused'].split('M')[0]) >= 6000
                ):
                    toobig.append(userinfo['user'])
                if userinfo['suspended'] == 1:
                    try:
                        print(
                            red(f'Unable to transfer {userinfo["user"]}'),
                            red('because it is suspended.'),
                        )
                        pickerlist.remove(
                            '{} - {} - {}'.format(
                                userinfo['user'],
                                userinfo['domain'],
                                userinfo['diskused'],
                            )[:50]
                        )
                    except Exception:
                        pass
        except KeyError:
            print(userdata)
            sys.exit('Unable to gather required information')
        if toobig:
            print(
                'The Following Accounts are too big to move by default.',
                'You can use --nosize if you need to transfer them',
            )
            print(*toobig)
            if not prompt_y_n('Would you like to continue?'):
                sys.exit('Try again with --nosize to ignore 6GB limit')
        if parsed.all:
            opts = [i.split(' ')[0] for i in pickerlist]
        else:
            useroptions = [i.split(' ')[0] for i in opts]
            print("Select users to transfer")

            print(opts.join("\n"))
            print("send q to finish")
            opts = []
            while True:
                user_input = input().lower()
                if user_input in useroptions and user_input not in opts:
                    opts.append(user_input)
                elif user_input == "q":
                    break
                split = user_input.split(" ")
                if len(split) > 1:
                    for name in split:
                        if name in useroptions and name not in opts:
                            opts.append(name)

        os.system('clear')
        print(opts)
        sys.stdout.flush()
        status = {}
        for user in opts:
            status[user] = 'Pending'
        for user in opts:
            print_status_info(status)
            try:
                if (
                    userdata[user]['disklimit'].lower() != 'unlimited'
                    and str(userdata[user]['diskused'].split('M')).lower()
                    != 'none'
                ):
                    if (
                        int(userdata[user]['diskused'].split('M')[0]) * 2
                    ) < userdata[user]['disklimit'].split('M')[0]:
                        print(
                            red(f'Warning: User {user}: Does not have double'),
                            red('the quota of the disk space used which can '),
                            red('cause problems'),
                        )
                        print(
                            green('Setting Quota to unlimited to ensure no '),
                            green(f'problem with backup on {user}'),
                        )
                        newquota = remote_whm_post(
                            parsed,
                            'json-api/modifyacct',
                            {'api.version': 1, 'user': user, 'QUOTA': 0},
                        )
                        if newquota['metadata']['result'] == 1:
                            print(green('Result: Success'))
            except Exception as exc:
                print(exc)
                print(red(f'Failed to set new Quota on {user}'))
            backup = get_cpanel_backup(
                parsed, restoreuser=user, restoredomain=userdata[user]['domain']
            )
            if backup and not parsed.norestore:
                backup = set_backup_to_standard_naming(backup, user)
                status[user] = restore_backup(backup, user, parsed)
            elif parsed.norestore:
                status[user] = 'Restore Skipped'
            else:
                status[user] = 'Failed'
        print_status_info(status)
        print('Done')


def print_status_info(status):
    tbl = PrettyTable()
    tbl.field_names = ["User", "Status", "Conflict Info"]
    for user, itemstatus in status.items():
        if '-' in status:
            conflict = status.split('-')[1:]
            itemstatus = status.split('-')[0]
        else:
            conflict = None
        tbl.add_row([user, itemstatus, conflict])
    print(tbl)


if __name__ == "__main__":
    main()

Zerion Mini Shell 1.0