Mini Shell
#!/opt/imh-python/bin/python3
# post CloudLinux user stats to grafana
from argparse import ArgumentParser
import subprocess
import json
import requests
import traceback
from socket import gethostname
from datetime import datetime
from pathlib import Path
from time import time
maint_dir = Path("/opt/maint/etc")
GLOBAL_IDS = {}
def run_script(command, capture_output=True, check_exit_code=True):
proc = subprocess.run(command, capture_output=capture_output)
out = proc.stdout
if proc.returncode != 0 and check_exit_code:
raise Exception(
f"Command returned exit code {proc.returncode}" f": '{command}'"
)
return out.decode("utf-8")
class StatsAPI:
def __init__(self, conf):
self.url = conf["host"]
self.key = conf["key"]
def post_stats(self, stats, period):
server = gethostname()
data = dict(server=gethostname(), period=period, stats=stats)
response = requests.post(
f"{self.url}/v1/userstats/?server={server.split('.')[0]}",
json=data,
headers={"Authorization": f"{self.key}"},
)
return response
def fatal_error(msg):
print(f"Error: {msg}")
msg = f"{datetime.now()}: {msg}"
err_file = maint_dir / "userstats.err"
footer = (
"Remove this file if you've reviewed the error message"
"or fixed the issue."
)
err_file.write_text(f"{msg}\n\n{footer}")
exit(1)
def load_conf():
conf = maint_dir / "userstats.cfg"
if not conf.exists():
fatal_error(f"Configuration file {conf} not found.")
return json.loads(conf.read_text())
def parse_args():
parser = ArgumentParser(description="Export user usage statistics")
parser.add_argument(
"--check",
action="store_true",
)
return parser.parse_args()
def check():
err_file = maint_dir / "userstats.err"
status = run_script(["systemctl", "status", "lvestats"])
if "Active: active" not in status:
print("CRITICAL: lvestats is not running.")
exit(2) # CRITICAL
if err_file.exists():
now = datetime.now().astimezone().timestamp()
delta = now - err_file.stat().st_mtime
if delta < 60 * 15: # 5 minutes
print(
f"CRITICAL: Userstats failfile tripped. Error in: {err_file} "
"When the issue is fixed - the command runs normally - remove the file"
)
exit(2) # CRITICAL
else:
print(
f"WARNING: {err_file} exists and is {int(delta/60)} minutes old."
)
exit(1) # WARNING
if "ERROR" in status:
print("WARNING: lvestats service encountered an error.")
exit(1) # WARNING
print("OK.")
exit(0)
def detect_period(average_period: int, touch_threshold=False):
"""
Use touch file to detect period
touch_threshold determines if we should touch the file regardless of
average_period. True means we only touch it if delta is > average_period.
"""
touch_file = Path(f"/run/clstats-touch-{average_period}")
if not touch_file.exists():
touch_file.touch()
return average_period
else:
mtime = int(touch_file.stat().st_mtime)
now = int(time())
time_delta = now - mtime
minutes = int(round(time_delta / 60))
if not touch_threshold or minutes >= average_period:
touch_file.touch()
return max(average_period, minutes)
else:
return minutes
def get_stats(period):
stats = [
"/sbin/cloudlinux-statistics",
"--period",
period,
"--json",
]
usage_stats = run_script(stats)
usage_data = json.loads(usage_stats)
rows = []
userdata = usage_data["users"]
for user_details in userdata:
usage = user_details["usage"]
limits = user_details["limits"]
faults = user_details["faults"]
username = user_details["username"]
domain = user_details["domain"]
reseller = user_details["reseller"]
row = {}
def convert_int(stat):
return min(int(round(stat["lve"])), 2147483647)
usage_convert_table = {
"cpu": lambda x: min(int(round(x["lve"])), 65535),
"ep": lambda x: min(int(x["lve"]), 65535),
"io": lambda x: min(int(x["lve"] / 1024 / 1024), 65535), # mb
"iops": lambda x: min(int(x["lve"]), 65535),
"pmem": lambda x: min(int(x["lve"] / 1024 / 1024), 65535), # mb
"vmem": lambda x: min(int(x["lve"] / 1024 / 1024), 65535), # mb
"nproc": lambda x: min(int(x["lve"]), 65535),
}
row["usage"] = {}
for key, value in usage.items():
converted = usage_convert_table[key](value)
key_name = key
if key in ["io", "pmem", "vmem"]:
key_name = f"{key}_m"
row["usage"][key_name] = converted
row["limits"] = {}
for key, value in limits.items():
row["limits"][key] = convert_int(value)
row["faults"] = {}
for key, value in faults.items():
row["faults"][key] = convert_int(value)
row["domain"] = domain
row["reseller"] = reseller
row["username"] = username
rows.append(row)
return rows
def main():
args = parse_args()
if args.check:
check()
if not Path("/sbin/cloudlinux-statistics").exists():
fatal_error(
"cloudLinux-statistics not found. "
"This tool only works on CloudLinux systems."
)
conf = load_conf()
try:
api = StatsAPI(conf["api"])
period = detect_period(1)
rows = get_stats(f"{period}m")
if rows:
post1 = api.post_stats(rows, 1)
if post1.status_code != 200:
fatal_error(
f"Failed to post user stats to Grafana: {post1.status_code}\n{post1.text}"
)
except Exception as e:
stack_trace = traceback.format_exc()
fatal_error(f"An error occurred: {str(e)}\n{stack_trace}")
if __name__ == "__main__":
main()
Zerion Mini Shell 1.0