Mini Shell
Direktori : /opt/sharedrads/ |
|
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