Mini Shell
Direktori : /opt/sharedrads/ |
|
Current File : //opt/sharedrads/upgrade-check |
#!/opt/imh-python/bin/python3
# vim: set ts=4 sw=4 expandtab syntax=python:
# the dash in this script's filename sets off invalid-name
# pylint: disable=invalid-name
"""Change plan eligibility checker tool"""
from pathlib import Path
import sys
import json
import logging
from argparse import ArgumentParser
from cpapis import uapi, cpapi2, CpAPIError
__version__ = "1.1.0"
__date__ = "08 Nov 2021"
logger = logging.getLogger('upgrade-check')
def setup_logging(clevel=logging.INFO, flevel=logging.DEBUG, logfile=None):
"""
Setup logging
"""
logger.setLevel(logging.DEBUG)
# Console
con = logging.StreamHandler()
con.setLevel(clevel)
con_format = logging.Formatter("%(levelname)s: %(message)s")
con.setFormatter(con_format)
logger.addHandler(con)
# File
if logfile:
try:
flog = logging.handlers.WatchedFileHandler(logfile)
flog.setLevel(flevel)
flog_format = logging.Formatter(
"[%(asctime)s] %(name)s: %(levelname)s: %(message)s"
)
flog.setFormatter(flog_format)
logger.addHandler(flog)
except Exception as e:
logger.warning("Failed to open logfile: %s", str(e))
def parse_cli(show_help=False):
"""Parse CLI arguments"""
parser = ArgumentParser(description="Change plan eligibility checker tool")
# fmt: off
parser.set_defaults(user=None, showmail=False)
parser.add_argument('user', metavar="USER", help="Username to check")
parser.add_argument(
'--plan', '-p', metavar="PLAN", help="Plan to check against"
)
parser.add_argument(
'--showmail', '-m', action='store_true',
help="Show detailed email usage",
)
parser.add_argument(
'--debug', '-d', dest='loglevel',
action='store_const', const=logging.DEBUG,
help="Enable debug output",
)
parser.add_argument(
'--version', '-v', action='version',
version=f"{__version__} ({__date__})",
)
# fmt: on
if show_help:
parser.print_help()
sys.exit(1)
return parser.parse_args()
def format_size(insize, rate=False, bits=False):
"""format human-readable file size and xfer rates"""
onx = float(abs(insize))
for u in ['B', 'K', 'M', 'G', 'T', 'P']:
if onx < 1024.0:
tunit = u
break
onx /= 1024.0
suffix = ""
if tunit != 'B':
suffix = "iB"
if rate:
if bits:
suffix = "bps"
onx *= 8.0
else:
suffix += "/sec"
if tunit == 'B':
ostr = "%3d %s%s" % (onx, tunit, suffix)
else:
ostr = f"{onx:3.01f} {tunit}{suffix}"
return ostr
def mkpct(ival, tot):
"""Make value/percent string"""
try:
po = f"{ival:d} ({(float(ival) / float(tot)) * 100.0:3.01f}%)"
except Exception:
po = "0 (-.-%)"
return po
def get_account_summary(username):
"""Call UAPI to get domain, DB, and other usage info"""
try:
ret = uapi('DomainInfo::list_domains', user=username, check=True)
except CpAPIError as exc:
logger.error(str(exc))
return None
return ret['result']['data']
def get_db_count(username):
"""Get mySQL database count"""
try:
with open(
Path('/var/cpanel/datastore', username, 'mysql-db-count'),
encoding='ascii',
) as file:
dcount = int(file.read().strip())
except Exception as e:
logger.error("Failed to get DB count: %s", str(e))
return None
return dcount
def get_disk_usage(username):
"""Check filesystem quotas for usage"""
try:
ret = cpapi2('DiskUsage::fetchdiskusage', user=username)
except CpAPIError as exc:
logger.error(str(exc))
return None
try:
usage = int(ret['cpanelresult']['data'][0]['contained_usage'] / 2**20)
except Exception as exc:
logger.error("Failed to parse cpapi2 result: %s", exc)
return None
return usage
def get_mail_accounts(username):
try:
with open(
Path('/home', username, '.cpanel/email_accounts.json'),
encoding='ascii',
) as file:
eraw = file.read().strip()
if len(eraw) > 1:
ejson = json.loads(eraw)
else:
ejson = {}
logger.debug(
"email_accounts.json is empty. assuming 0 accounts."
)
except Exception as exc:
logger.error("failed to open email_accounts.json for user: %s", exc)
return None
accounts = []
for tdomain in ejson.keys():
if tdomain.startswith('_'):
continue
for taccount, tinfo in ejson[tdomain]['accounts'].items():
accounts.append(
{
'suspended': tinfo['suspended_login'],
'domain': tdomain,
'user': taccount,
'email': f"{taccount}@{tdomain}",
'used': int(tinfo['diskused']),
'quota': int(tinfo.get('diskquota', 0)),
}
)
return accounts
def print_accounts(accounts):
"""Print list of email accounts by size"""
print("\n************ Email Accounts ************")
print("{:40} {:>10} / {:>10}".format("Email", "DiskUsed", "Quota"))
for taccount in accounts:
print(
"{:40} {:>10} / {:>10}".format(
taccount['email'],
format_size(taccount['used']),
format_size(taccount['quota']),
)
)
def get_plan_params():
"""Read upgrade_params from file ./etc/upgrade_params.json"""
ppath = Path(__file__).resolve().parent / 'etc/upgrade_params.json'
try:
with open(ppath, encoding='ascii') as f:
params = json.load(f)['packages']
except Exception as e:
logger.error("Failed to read account params from file: %s", str(e))
return None
return params
def get_account_stats(domlist, dbcount, maillist, acctsize):
"""Calculate account stats from gathered params"""
if len(maillist) > 0:
maxmailsize = int(
max(x['used'] for x in maillist if not x['suspended'])
/ (1024 * 1024)
)
else:
maxmailsize = 0
account = {
'disk': acctsize,
'db': dbcount,
'addon': len(domlist['addon_domains']),
'parked': len(domlist['parked_domains']),
'sub': len(domlist['sub_domains']),
'mailacct': len([x for x in maillist if not x['suspended']]),
'maxmailsize': maxmailsize,
}
return account
def check_plan(stat, plan):
"""Compare user stats versus plandata"""
return (
_fits_plan(stat['disk'], plan['quota'])
and _fits_plan(stat['db'], plan['maxdb'])
and _fits_plan(stat['addon'], plan['maxaddon'])
and _fits_plan(stat['addon'], plan['maxaddon'])
and _fits_plan(stat['parked'], plan['maxpark'])
and _fits_plan(stat['sub'], plan['maxsub'])
and _fits_plan(stat['mailacct'], plan['maxpop'])
and _fits_plan(stat['maxmailsize'], plan['max_emailacct_quota'])
)
def _fits_plan(value, quota):
if quota == -1:
return True
return value <= quota
def check_one_plan(astat, plan):
"""Check a single plan and return yes/no"""
try:
plandata = PLANS[plan]
except Exception:
logger.error("Plan '%s' does not exist", plan)
return None
if check_plan(astat, plandata):
print("yes")
return True
print("no")
return False
def yesno(ibool):
"""Colorize yes/no from true/false"""
if ibool:
return "\033[92mYES\033[0m"
return "\033[91mNO\033[0m"
def check_all_plans(username, astat):
"""Print a table of all compatible plans"""
print(f"** Usage summary for user '{username}' ***")
for tkey, tval in astat.items():
print(f"{tkey:16} {tval}")
print("")
print(f"** Plan compatibility for user '{username}' ***")
for tplan, plandata in PLANS.items():
print(f"{tplan:30} {yesno(check_plan(astat, plandata))}")
def main():
"""Entry point"""
setup_logging()
args = parse_cli()
domains = get_account_summary(args.user)
dbcount = get_db_count(args.user)
mail_accounts = get_mail_accounts(args.user)
diskuse = get_disk_usage(args.user)
if (
domains is None
or dbcount is None
or mail_accounts is None
or diskuse is None
):
logger.error(
"Failed to gather data for user. Ensure username is "
"correct or check account manually."
)
sys.exit(2)
astat = get_account_stats(domains, dbcount, mail_accounts, diskuse)
if args.plan:
check_one_plan(astat, args.plan)
else:
check_all_plans(args.user, astat)
if mail_accounts is None:
logger.error("Failed to get list of email accounts for user")
else:
if args.showmail:
print_accounts(mail_accounts)
PLANS = get_plan_params()
if __name__ == '__main__':
main()
Zerion Mini Shell 1.0