#!/opt/imh-python/bin/python3
"""Account-review: Shows relevant information about cPanel users"""
from prettytable import PrettyTable
from cpapis import whmapi1
from rads import color, UserData, CpuserError
'ns.inmotionhosting.com',
'ns1.inmotionhosting.com', # there's a cname to ns
'ns2.inmotionhosting.com',
# IOError: file unreadable
# KeyError: expected key is missing from dict
# AttributeError: tried to run .values(), etc on non-dict
# TypeError: tried to iterate on a None
BAD_YAML = (IOError, ValueError, KeyError, AttributeError, TypeError)
BAD_API = (ValueError, KeyError, AttributeError, TypeError)
def errprint(*errmsgs, **kwargs):
"""deprecated, copied from rads.common"""
errmsgs = [color.red(x) for x in errmsgs]
fatal = kwargs.pop('fatal')
print(*errmsgs, file=sys.stderr, **kwargs)
"""Convert size, in MB, to human GB/MB notation"""
size_int = int(float(str(size).rstrip('M')))
return f'{size_int / 1024.0:.1f} GB'
def fake_header(table, header):
"""Supplied a prettytable object, form a fake header"""
width = len(str(table).splitlines()[0]) - 2
print('+', '-' * width, '+', sep='')
print(f'|{color.bold(header.center(width))}|')
def get_users_from_args():
"""Parse a list of users to display from CLI args or working dir"""
restricted = rads.OUR_RESELLERS
restricted.extend(rads.SYS_USERS)
secure_user = rads.SECURE_USER
if secure_user is not None:
restricted.append(secure_user)
argv.append(os.getcwd().split('/')[2])
if not rads.is_cpuser(user):
errprint(user, 'is not a valid cPanel user')
errprint(user, 'is a restricted user')
errprint('no users to display', fatal=True)
def check_modsec_custom(user):
"""Check for Apache customizations"""
if os.path.isfile('/etc/cpanel/ea4/is_ea4'):
basepath = '/etc/apache2/conf.d/userdata/std/2*'
basepath = '/usr/local/apache/conf/userdata/std/2'
conf_paths = os.path.join(basepath, user, '*/modsec.conf')
return len(glob.glob(conf_paths)) > 0
"""Check to see if user's homedir exists"""
return os.path.isdir(os.path.expanduser("~%s" % user))
def check_email_suspension(user):
Checks /etc/outgoing_mail_suspended_users for passed user
Returns True if found, otherwise returns False.
with open('/etc/outgoing_mail_suspended_users') as file:
def count_databases(user):
"""Get number of databases"""
paths.append(os.path.join('/var/cpanel/datastore', user, 'mysql-db-count'))
os.path.join('/home', user, '.cpanel/datastore/mysql-db-count')
return file.read().strip()
def display_user(user, shared_ips):
"""Display one user. Called per argument by main()"""
# domin info / ssl and doc roots
except CpuserError as exc:
'See http://wiki.inmotionhosting.com/index.php?title=Account-review#Troubleshooting'
# get SSL status of primary domain
if udata.primary.has_ssl:
acct_summ = whmapi1('accountsummary', {"user": user})['data']['acct'][0]
errprint('Error running "accountsummary" in WHM API')
usrtable = PrettyTable(['1', '2', '3'])
usrtable.align['1'] = 'r'
usrtable.align['2'] = 'l'
usrtable.align['3'] = 'l'
usrtable.add_row(['Created on', acct_summ['startdate'], ''])
if acct_summ['owner'] == user:
owner_text = color.yellow('Self')
elif acct_summ['owner'] == 'root':
owner_text = color.red('Issue')
usrtable.add_row(['Owner', acct_summ['owner'], owner_text])
usrtable.add_row(['cPanel Contact', acct_summ['email'], ''])
usrtable.add_row(['Account Size', conv_size(acct_summ['diskused']), ''])
usrtable.add_row(['MySQL Databases', count_databases(user), ''])
if acct_summ['ip'] in shared_ips:
ip_text = color.red('Dedicated')
usrtable.add_row(['IP', acct_summ['ip'], ip_text])
usrtable.add_row(['SSL', ssl_text, ''])
if udata.primary.docroot == os.path.join('/home', user, 'public_html'):
docroot_text = 'Standard'
docroot_text = color.red('Custom')
usrtable.add_row(['Docroot', udata.primary.docroot, docroot_text])
if check_modsec_custom(user):
modsec_text = color.red('Modified')
usrtable.add_row(['Modsec', modsec_text, ''])
homedir_text = os.path.expanduser("~%s" % user)
homedir_text = color.red('Missing')
usrtable.add_row(['Homedir', homedir_text, ''])
# We print out our own fake header, not the one supplied by prettytable
header = '{0.user} / {0.primary.domain} / {1[plan]}'.format(
fake_header(usrtable, header)
if acct_summ['suspended']:
print(color.red('Suspended:'), acct_summ['suspendreason'].strip())
# Check for outgoing email suspension
if check_email_suspension(user):
print(color.red('Outgoing Email Suspended: Yes'))
print('Outgoing Email Suspended: No')
# Print Addon/Parked/Sub domains
# This is all handled by the same table to align them all the same
dom_table = PrettyTable(cols)
dom_table.align['labelcol'] = 'r'
dom_table.align['dom'] = 'l'
dom_table.align['root'] = 'l'
table_labels = {'Addon': 'addons', 'Parked': 'parked', 'Sub': 'subs'}
for label, lblkey in table_labels.items():
if len(getattr(udata, lblkey)) == 0:
# the fake "header" to display between each type of domain
dom_table.add_row(['', '', '', '', '', '', ''])
dom_table.add_row(head_row)
# iterate over each domain of this type
for dom in getattr(udata, lblkey):
ssl_label = color.magenta('Has SSL:')
ssl_label = color.magenta('Has SSL:')
color.magenta('Domain:'),
color.magenta('Docroot:'),
reseller = user in whmapi1("listresellers")['data']['reseller']
errprint(f'Error obtaining reseller list from WHM API. {e}', fatal=True)
res_stats = whmapi1('resellerstats', {"user": user})['data']['reseller']
errprint('Error running "resellerstats" API function', fatal=True)
res_table = PrettyTable(['1', '2', '3'])
res_table.align['1'] = 'r'
res_table.align['2'] = 'l'
res_table.align['3'] = 'l'
res_table.add_row(['Child Count', len(res_stats['acct']), ''])
if acct_summ['owner'] != user:
diskused = float(acct_summ['diskused'][:-1])
used_raw = acct_summ['diskused']
'Unexpected value for "diskused" in accountsummary for', user
errprint(rf"Expcted to match regex '^\d+M$' but got {used_raw}")
combined = conv_size(res_stats['diskused'] + diskused)
res_table.add_row(['Combined Size', combined, ''])
['Combined Size', conv_size(res_stats['diskused']), '']
for index, (nsname, nstext) in enumerate(get_nameservers(user)):
res_table.add_row([f'Nameserver {index + 1}', nsname, nstext])
for index, (dip, diptext) in enumerate(get_ip_pool(user, shared_ips)):
res_table.add_row([f'Pool IP {index + 1}', dip, diptext])
fake_header(res_table, f'Reseller Stats for {user}')
child_table = PrettyTable(
['Child', 'Domain', 'Size', 'Suspended', 'Deleted']
child_table.align['Domain'] = 'l'
child_table.align['Size'] = 'r'
for acct in res_stats['acct']:
susp_text = color.red('yes')
if check_email_suspension(acct['user']):
susp_text = color.red('Outbound Email')
del_text = color.red('yes†')
user_text = color.yellow(user)
conv_size(acct['diskused']),
fake_header(child_table, f'Child accounts of {user}')
+ "Deleted accounts cannot be recovered without a backup."
def get_ip_pool(user, shared_ips):
"""Get IP pool for a reseller and status text for each"""
dips_path = os.path.join('/var/cpanel/dips', user)
with open(dips_path) as file:
dips = file.read().split()
for reseller in os.listdir('/var/cpanel/dips'):
other_path = os.path.join('/var/cpanel/dips', reseller)
with open(other_path) as file:
others.update(file.read().split())
errprint('Could not read', other_path)
res_main_path = os.path.join('/var/cpanel/mainips', user)
with open(res_main_path) as file:
res_main = file.read().split()
errprint('Could not read', res_main_path)
ip_msgs.append('Pool Main')
ip_msgs.append(color.red('Reseller Conflict'))
ip_msgs.append(color.red('Server Main'))
ip_pool.append([dip, '\n'.join(ip_msgs)])
def get_nameservers(user):
"""Get nameservers for a reseller and status text for each"""
with open('/var/cpanel/resellers-nameservers') as file:
data = file.read().splitlines()
errprint('could not read /var/cpanel/resellers-nameservers')
if not line.startswith(starter):
raw_nsnames = line[len(starter) :].strip().split(',')
for nsname in raw_nsnames:
nsinfo.append([nsname, ns_custom(nsname)])
errprint('user missing from /var/cpanel/resellers-nameservers')
"""Try to determine if a nameserver is custom"""
if nsname not in STANDARD_NS:
return color.red('Custom')
if len(fqdn.split('.')) != 3:
return color.yellow('Custom? Server fqdn setup wrong')
fqdn_split = fqdn.split('.')
dom = '.'.join(fqdn_split)
return color.yellow('Server FQDN mismatch')
def cpanel_display_user(users):
"""Main loop - run display_user on each user"""
with open('/var/cpanel/mainip') as handle:
shared_ips = [handle.read().strip()]
with open('/var/cpanel/mainips/root') as handle:
shared_ips.extend(handle.read().split())
pass # this file may not exist; that is okay
display_user(user, shared_ips)