Edit File by line
/home/barbar84/public_h.../wp-conte.../plugins/sujqvwi/AnonR/smanonr..../opt/sharedra...
File: move_generator.py
#!/opt/imh-python/bin/python3
[0] Fix | Delete
"""Disk Move Generator - generates disk move tickets
[1] Fix | Delete
according to arguments and exclusions"""
[2] Fix | Delete
from operator import itemgetter
[3] Fix | Delete
from platform import node
[4] Fix | Delete
import datetime
[5] Fix | Delete
import argparse
[6] Fix | Delete
import sys
[7] Fix | Delete
from pathlib import Path
[8] Fix | Delete
from concurrent.futures import ThreadPoolExecutor, as_completed
[9] Fix | Delete
from typing import Union
[10] Fix | Delete
import arrow
[11] Fix | Delete
import yaml
[12] Fix | Delete
from tabulate import tabulate
[13] Fix | Delete
import rads
[14] Fix | Delete
[15] Fix | Delete
EXCLUSION_LIST = Path('/var/log/disk_exclude')
[16] Fix | Delete
TIMER = 30 # days before a user is removed from the exclusion list
[17] Fix | Delete
[18] Fix | Delete
[19] Fix | Delete
def get_args():
[20] Fix | Delete
"""Parse arguments"""
[21] Fix | Delete
parser = argparse.ArgumentParser(description=__doc__)
[22] Fix | Delete
group = parser.add_mutually_exclusive_group()
[23] Fix | Delete
[24] Fix | Delete
parser.add_argument(
[25] Fix | Delete
'-a',
[26] Fix | Delete
'--add',
[27] Fix | Delete
type=str,
[28] Fix | Delete
dest='add_user',
[29] Fix | Delete
nargs='+',
[30] Fix | Delete
default=[],
[31] Fix | Delete
help="Add user to exclusion list. Entries survive 30 days",
[32] Fix | Delete
)
[33] Fix | Delete
parser.add_argument(
[34] Fix | Delete
'-m',
[35] Fix | Delete
'--min',
[36] Fix | Delete
type=int,
[37] Fix | Delete
default=8,
[38] Fix | Delete
help="Minimum size of account to migrate in GB, default 8",
[39] Fix | Delete
)
[40] Fix | Delete
parser.add_argument(
[41] Fix | Delete
'-x', '--max', type=int, help="Maximum size of account to migrate in GB"
[42] Fix | Delete
)
[43] Fix | Delete
parser.add_argument(
[44] Fix | Delete
'-t',
[45] Fix | Delete
'--total',
[46] Fix | Delete
type=int,
[47] Fix | Delete
help="Lists several eligible accounts whose size totals up to X GB",
[48] Fix | Delete
)
[49] Fix | Delete
group.add_argument(
[50] Fix | Delete
'-e',
[51] Fix | Delete
'--exclude',
[52] Fix | Delete
type=str,
[53] Fix | Delete
dest='exc_user',
[54] Fix | Delete
nargs='+',
[55] Fix | Delete
default=[],
[56] Fix | Delete
help="List of users to exclude alongside exclusion list",
[57] Fix | Delete
)
[58] Fix | Delete
group.add_argument(
[59] Fix | Delete
'-n',
[60] Fix | Delete
'--noexclude',
[61] Fix | Delete
action="store_true",
[62] Fix | Delete
help="Do not use exclusion list",
[63] Fix | Delete
)
[64] Fix | Delete
parser.add_argument(
[65] Fix | Delete
'-l',
[66] Fix | Delete
'--listaccount',
[67] Fix | Delete
action="store_true",
[68] Fix | Delete
help="Print list of eligible accounts",
[69] Fix | Delete
)
[70] Fix | Delete
parser.add_argument(
[71] Fix | Delete
'-d',
[72] Fix | Delete
'--ticket',
[73] Fix | Delete
action="store_true",
[74] Fix | Delete
help="Email eligible accounts to the disk moves queue",
[75] Fix | Delete
)
[76] Fix | Delete
args = parser.parse_args()
[77] Fix | Delete
# one of l, d, or a must be picked
[78] Fix | Delete
if args.ticket is False and args.listaccount is False and not args.add_user:
[79] Fix | Delete
print(
[80] Fix | Delete
"--ticket (-d), --listaccount (-l),",
[81] Fix | Delete
"or --add (-a) [user] is required",
[82] Fix | Delete
)
[83] Fix | Delete
sys.exit(1)
[84] Fix | Delete
return args
[85] Fix | Delete
[86] Fix | Delete
[87] Fix | Delete
def get_user_owners():
[88] Fix | Delete
"""Parse /etc/trueuserowners"""
[89] Fix | Delete
with open('/etc/trueuserowners', encoding='utf-8') as handle:
[90] Fix | Delete
user_owners = yaml.load(handle, rads.DumbYamlLoader)
[91] Fix | Delete
if user_owners is None:
[92] Fix | Delete
return {}
[93] Fix | Delete
return user_owners
[94] Fix | Delete
[95] Fix | Delete
[96] Fix | Delete
def main():
[97] Fix | Delete
args = get_args()
[98] Fix | Delete
[99] Fix | Delete
refresh_exclusion_list()
[100] Fix | Delete
[101] Fix | Delete
# If user to be added to exclusion list
[102] Fix | Delete
if args.add_user:
[103] Fix | Delete
# and list or email also selected
[104] Fix | Delete
if args.listaccount or args.ticket:
[105] Fix | Delete
print("Adding to exclusion list first...")
[106] Fix | Delete
for user in args.add_user:
[107] Fix | Delete
if not rads.is_cpuser(user):
[108] Fix | Delete
print(f"{user} is not a valid cpanel user")
[109] Fix | Delete
args.add_user.remove(user)
[110] Fix | Delete
add_excluded_users(args.add_user)
[111] Fix | Delete
[112] Fix | Delete
if args.listaccount or args.ticket:
[113] Fix | Delete
# collect a list of lists containing the eligible users
[114] Fix | Delete
# and the total size
[115] Fix | Delete
accounts = collect_accounts(
[116] Fix | Delete
args.min, args.max, args.total, args.noexclude, args.exc_user
[117] Fix | Delete
)
[118] Fix | Delete
[119] Fix | Delete
if args.listaccount:
[120] Fix | Delete
list_accounts(accounts)
[121] Fix | Delete
[122] Fix | Delete
if args.ticket:
[123] Fix | Delete
email_accounts(accounts)
[124] Fix | Delete
[125] Fix | Delete
return args
[126] Fix | Delete
[127] Fix | Delete
[128] Fix | Delete
def get_exclusion_list() -> dict:
[129] Fix | Delete
'''Read from the exclusion list and return it as a dict'''
[130] Fix | Delete
data = {}
[131] Fix | Delete
try:
[132] Fix | Delete
with open(EXCLUSION_LIST, encoding='ascii') as exclusionlist:
[133] Fix | Delete
data: dict = yaml.load(exclusionlist)
[134] Fix | Delete
if not isinstance(data, dict):
[135] Fix | Delete
print("Error in exclusion list, rebuilding")
[136] Fix | Delete
data = {}
[137] Fix | Delete
write_exclusion_list(data)
[138] Fix | Delete
except (yaml.YAMLError, OSError) as exc:
[139] Fix | Delete
print(type(exc).__name__, exc, sep=': ')
[140] Fix | Delete
print('Recreating', EXCLUSION_LIST)
[141] Fix | Delete
write_exclusion_list(data)
[142] Fix | Delete
return data
[143] Fix | Delete
[144] Fix | Delete
[145] Fix | Delete
def add_excluded_users(users: list[str]):
[146] Fix | Delete
'''Format user information and timestamp for the exclusion list'''
[147] Fix | Delete
for user in users:
[148] Fix | Delete
exclusion_list = get_exclusion_list()
[149] Fix | Delete
exclusion_list[user] = arrow.now().int_timestamp
[150] Fix | Delete
write_exclusion_list(exclusion_list)
[151] Fix | Delete
print(f"{user} added to exclusion list")
[152] Fix | Delete
[153] Fix | Delete
[154] Fix | Delete
def write_exclusion_list(exclusion_list: dict[str, int]) -> None:
[155] Fix | Delete
'''Write to the exclusion list'''
[156] Fix | Delete
try:
[157] Fix | Delete
with open(EXCLUSION_LIST, 'w', encoding='ascii') as outfile:
[158] Fix | Delete
yaml.dump(exclusion_list, outfile, indent=4)
[159] Fix | Delete
except Exception:
[160] Fix | Delete
pass
[161] Fix | Delete
[162] Fix | Delete
[163] Fix | Delete
def refresh_exclusion_list() -> None:
[164] Fix | Delete
'''If a timeout has expired, remove the user'''
[165] Fix | Delete
try:
[166] Fix | Delete
timeouts = get_exclusion_list()
[167] Fix | Delete
new_dict = {}
[168] Fix | Delete
[169] Fix | Delete
for user in timeouts:
[170] Fix | Delete
if arrow.now().int_timestamp - timeouts[user] < int(
[171] Fix | Delete
datetime.timedelta(days=TIMER).total_seconds()
[172] Fix | Delete
):
[173] Fix | Delete
new_dict[user] = timeouts[user]
[174] Fix | Delete
write_exclusion_list(new_dict)
[175] Fix | Delete
except Exception:
[176] Fix | Delete
pass
[177] Fix | Delete
[178] Fix | Delete
[179] Fix | Delete
def initial_disqualify(
[180] Fix | Delete
user: str,
[181] Fix | Delete
*,
[182] Fix | Delete
min_size: int,
[183] Fix | Delete
max_size: int,
[184] Fix | Delete
noexclude: bool,
[185] Fix | Delete
exclusion_list: list[str],
[186] Fix | Delete
) -> tuple[str, Union[float, None]]:
[187] Fix | Delete
'''Run the user through the first gamut to determine if
[188] Fix | Delete
eligible for a move'''
[189] Fix | Delete
try:
[190] Fix | Delete
# knock out ineligible accounts
[191] Fix | Delete
if rads.cpuser_safe(user):
[192] Fix | Delete
return user, None
[193] Fix | Delete
if Path('/var/cpanel/suspended', user).is_file():
[194] Fix | Delete
return None
[195] Fix | Delete
if not noexclude and user in exclusion_list:
[196] Fix | Delete
return user, None
[197] Fix | Delete
[198] Fix | Delete
# get size
[199] Fix | Delete
size_gb: float = rads.QuotaCtl().getquota(user) / 2**30
[200] Fix | Delete
[201] Fix | Delete
# check for eligibility based on size
[202] Fix | Delete
if size_gb < min_size:
[203] Fix | Delete
return user, None
[204] Fix | Delete
if max_size and size_gb > max_size:
[205] Fix | Delete
return user, None
[206] Fix | Delete
# whatever's left after that, add to accounts list
[207] Fix | Delete
return user, size_gb
[208] Fix | Delete
except KeyboardInterrupt:
[209] Fix | Delete
# drop child proc if killed
[210] Fix | Delete
return user, None
[211] Fix | Delete
[212] Fix | Delete
[213] Fix | Delete
def collect_accounts(
[214] Fix | Delete
min_size: int,
[215] Fix | Delete
max_size: int,
[216] Fix | Delete
total_gb: int,
[217] Fix | Delete
noexclude: bool,
[218] Fix | Delete
exclude: list[str],
[219] Fix | Delete
) -> list[tuple[str, str, float]]:
[220] Fix | Delete
'''Get a list of users, and then eliminate them based on suspension
[221] Fix | Delete
status, size, and eligibility based on options provided'''
[222] Fix | Delete
[223] Fix | Delete
# initializing everything
[224] Fix | Delete
size_total = 0
[225] Fix | Delete
accounts = []
[226] Fix | Delete
eligible_accounts = []
[227] Fix | Delete
final_list = []
[228] Fix | Delete
[229] Fix | Delete
# gather exclusion lists
[230] Fix | Delete
try:
[231] Fix | Delete
exclusion_list = list(get_exclusion_list().keys())
[232] Fix | Delete
except Exception as exc:
[233] Fix | Delete
print(f"Skipping exception file - {type(exc).__name__}: {exc}")
[234] Fix | Delete
exclusion_list = []
[235] Fix | Delete
exclusion_list += exclude
[236] Fix | Delete
[237] Fix | Delete
# create child processes to run through the eligibility checks
[238] Fix | Delete
kwargs = dict(
[239] Fix | Delete
min_size=min_size,
[240] Fix | Delete
max_size=max_size,
[241] Fix | Delete
noexclude=noexclude,
[242] Fix | Delete
exclusion_list=exclusion_list,
[243] Fix | Delete
)
[244] Fix | Delete
accounts = []
[245] Fix | Delete
user_owners = get_user_owners()
[246] Fix | Delete
with ThreadPoolExecutor(max_workers=4) as pool:
[247] Fix | Delete
try:
[248] Fix | Delete
jobs = []
[249] Fix | Delete
for user, owner in user_owners.items():
[250] Fix | Delete
jobs.append(pool.submit(initial_disqualify, user, **kwargs))
[251] Fix | Delete
for future in as_completed(jobs):
[252] Fix | Delete
user, size_gb = future.result()
[253] Fix | Delete
if size_gb is not None:
[254] Fix | Delete
owner = user_owners[user]
[255] Fix | Delete
accounts.append((user, owner, size_gb))
[256] Fix | Delete
except KeyboardInterrupt:
[257] Fix | Delete
print("Caught KeyboardInterrupt.")
[258] Fix | Delete
pool.shutdown(wait=False, cancel_futures=True)
[259] Fix | Delete
return []
[260] Fix | Delete
if not accounts:
[261] Fix | Delete
return final_list
[262] Fix | Delete
# if anything survived those criteria...
[263] Fix | Delete
accounts.sort(key=itemgetter(2), reverse=True) # sort by size, descending
[264] Fix | Delete
# get a list of accounts of size > total
[265] Fix | Delete
if total_gb:
[266] Fix | Delete
size_total = 0
[267] Fix | Delete
for account in accounts:
[268] Fix | Delete
if len(eligible_accounts) < 3 or size_total < total_gb:
[269] Fix | Delete
eligible_accounts.append(account)
[270] Fix | Delete
size_total += account[2]
[271] Fix | Delete
else:
[272] Fix | Delete
break
[273] Fix | Delete
accounts = eligible_accounts
[274] Fix | Delete
final_list = accounts[:25]
[275] Fix | Delete
return final_list
[276] Fix | Delete
[277] Fix | Delete
[278] Fix | Delete
def list_accounts(accounts):
[279] Fix | Delete
'''Print the list of eligible accounts in a pretty table'''
[280] Fix | Delete
if not accounts:
[281] Fix | Delete
print("No accounts match criteria.")
[282] Fix | Delete
return
[283] Fix | Delete
print(
[284] Fix | Delete
tabulate(
[285] Fix | Delete
reversed(accounts),
[286] Fix | Delete
headers=["User", "Owner", "Size (GB)"],
[287] Fix | Delete
floatfmt=".1f",
[288] Fix | Delete
)
[289] Fix | Delete
)
[290] Fix | Delete
if total := sum(x[2] for x in accounts):
[291] Fix | Delete
print(f"Total size of matching accounts: {total:.2f} GB")
[292] Fix | Delete
[293] Fix | Delete
[294] Fix | Delete
def email_accounts(accounts: list[tuple[str, str, float]]):
[295] Fix | Delete
'''Send an email for each user in accounts'''
[296] Fix | Delete
server = node().split(".")[0]
[297] Fix | Delete
exclude = []
[298] Fix | Delete
for user, _, size_gb in accounts:
[299] Fix | Delete
mail_disk_move(user, server, size_gb)
[300] Fix | Delete
exclude.append(user)
[301] Fix | Delete
add_excluded_users(exclude)
[302] Fix | Delete
[303] Fix | Delete
[304] Fix | Delete
def mail_disk_move(username: str, server: str, size_gb: float):
[305] Fix | Delete
'''Sends email to the disk moves queue'''
[306] Fix | Delete
to_addr = "moves@imhadmin.net"
[307] Fix | Delete
subject = f"DISK MOVE: {username} @ {server}"
[308] Fix | Delete
body = f"""
[309] Fix | Delete
A new server disk move is required for {username} @ {server}
[310] Fix | Delete
[311] Fix | Delete
Move Username: {username}
[312] Fix | Delete
Account Size: {size_gb} GiB
[313] Fix | Delete
[314] Fix | Delete
Please review the account to determine if they are eligible for a migration:
[315] Fix | Delete
* hasn't been moved recently (e.g. in the past year/no current move scheduled)
[316] Fix | Delete
* is not storing ToS content
[317] Fix | Delete
* is not large to the point of absurdity
[318] Fix | Delete
* other reasons left to the discretion of the administrator
[319] Fix | Delete
[320] Fix | Delete
If the account is not eligible for a migration, please add them to the
[321] Fix | Delete
exception list to prevent further tickets being generated for their account:
[322] Fix | Delete
move_generator.py -a {username}
[323] Fix | Delete
[324] Fix | Delete
If the account is not eligible for reasons of ToS content, please respond
[325] Fix | Delete
to this ticket with the relevant information and leave it open to address
[326] Fix | Delete
further. For convenience, you may also update the subject line with the
[327] Fix | Delete
date on which this should be addressed again and/or notice count.
[328] Fix | Delete
"""
[329] Fix | Delete
if rads.send_email(to_addr, subject, body):
[330] Fix | Delete
print(f"Disk move tickets sent for {username}.")
[331] Fix | Delete
else:
[332] Fix | Delete
print(
[333] Fix | Delete
"Sending of disk_move ticket failed. You can use the -l option to",
[334] Fix | Delete
"view eligible accounts to move",
[335] Fix | Delete
)
[336] Fix | Delete
[337] Fix | Delete
[338] Fix | Delete
if __name__ == "__main__":
[339] Fix | Delete
main()
[340] Fix | Delete
[341] Fix | Delete
It is recommended that you Edit text format, this type of Fix handles quite a lot in one request
Function