Mini Shell
Direktori : /opt/sharedrads/ |
|
Current File : //opt/sharedrads/check_mysql |
#!/opt/imh-python/bin/python3
"""MySQL RADS tool - T2S version.
This script reads the dbindex file used by cPanel. If it cannot detect
databases owners correctly, try running /usr/local/cpanel/bin/setupdbmap"""
import argparse
import configparser
from operator import itemgetter
import json
import sys
from collections import Counter
import functools
import subprocess
from typing import Union
import pymysql
from pymysql.cursors import Cursor, DictCursor
from pymysql.optionfile import Parser as PyMySQLParser
import rads
from rads.color import red
connect = functools.partial(
pymysql.connect,
read_default_file='/root/.my.cnf',
database='INFORMATION_SCHEMA',
)
DBINDEX_PATH = '/var/cpanel/databases/dbindex.db.json'
def get_dbnames(owners: set[str]) -> set[str]:
"""Return a dict of {database: owner}"""
try:
with open(DBINDEX_PATH, encoding='ascii') as handle:
dbindex: dict = json.load(handle)['MYSQL']
except (ValueError, OSError, TypeError, KeyError):
print(
red(f"{DBINDEX_PATH} is unreadable. Try setupdbmap"),
file=sys.stderr,
)
sys.exit(2)
return {k for k, v in dbindex.items() if v in owners}
def kill(targets: set[str]):
"""Kill all MySQL queries for specific cPanel user(s)"""
db_names = get_dbnames(targets)
with connect() as conn:
query_ids = get_query_ids(conn, db_names=db_names)
kill_queries(conn, query_ids)
def killdb(targets: set[str]):
"""Kill all MySQL queries for specific database(s)"""
with connect() as conn:
query_ids = get_query_ids(conn, db_names=targets)
kill_queries(conn, query_ids)
def killqueries():
"""Kill ALL running queries (use with caution. This kills INSERTs!)"""
if not rads.prompt_y_n(
red(
'Use --killselects instead of --killqueries whenever possible!\n'
'--killqueries kills INSERT and UPDATE queries and can cause '
'data loss.\nProceed with --killqueries?'
)
):
print('canceled.')
sys.exit(0)
with connect() as conn:
kill_queries(conn, get_query_ids(conn))
def killselects():
"""Kill all running SELECT queries (safer than killqueries,
but use with caution)"""
with connect() as conn:
kill_queries(conn, get_query_ids(conn, selects_only=True))
def count_conns() -> int:
num = 0
with subprocess.Popen(
['ss', '-Hxtn'], stdout=subprocess.PIPE, encoding='ascii'
) as proc:
for line in proc.stdout:
try:
_, state, _, _, local, *_ = line.split()
except ValueError:
continue
if state not in ('ESTAB', 'CONNECTED'):
continue
if not local.endswith(':3306') and 'mysql' not in local:
continue
num += 1
if rcode := proc.returncode:
print(red(f'ss -Hxtn exited with error {rcode}'), file=sys.stderr)
return num
def get_max_connections() -> int:
default = 100
try:
parser = PyMySQLParser(strict=False)
if not parser.read('/etc/my.cnf'):
return default
return int(parser.get('mysqld', 'max_connections'))
except configparser.Error:
return default
def sockets() -> None:
"""Show number of open connections vs max connections"""
# Use ss and my.cnf since this is usually used while MySQL is hurting
conns = count_conns()
max_conns = get_max_connections()
print('MySQL Connections:', conns, '/', max_conns)
def active() -> None:
"""Display users with the most currently running queries"""
with connect() as conn, conn.cursor() as cur:
cur: Cursor
cur.execute('SELECT `USER` FROM `PROCESSLIST`')
users = cur.fetchall()
counts = Counter(map(itemgetter(0), users))
for user, count in sorted(counts.items(), key=itemgetter(1)):
print(user, count)
def parse_args() -> tuple[str, set[str]]:
"""Parse commandline arguments"""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
'targets',
type=str,
nargs='*',
default=[],
metavar='NAME',
help='List of users or DBs used with --kill or --killdb',
)
actions = parser.add_mutually_exclusive_group(required=True)
for func in (kill, killdb, killqueries, killselects, sockets, active):
actions.add_argument(
f'--{func.__name__}',
action='store_const',
const=func.__name__,
dest='func',
help=func.__doc__,
)
args = parser.parse_args()
return args.func, set(args.targets)
def kill_queries(conn: pymysql.Connection, id_list: list[int]) -> None:
"""Kill all queries provided in id_list"""
print(len(id_list), 'queries to kill')
if len(id_list) == 0:
return
with conn.cursor() as cur:
cur: Cursor
killed = errors = 0
for query_id in id_list:
try:
cur.execute('KILL %d' % query_id)
cur.fetchall()
killed += 1
except pymysql.Error:
errors += 1
print(killed, 'successfully killed.', errors, 'failed.')
def get_query_ids(
conn: pymysql.Connection,
db_names: Union[None, set[str]] = None,
selects_only: bool = False,
) -> list[int]:
"""Get query IDs to kill"""
with conn.cursor(DictCursor) as cur:
cur.execute('SELECT `ID`,`USER`,`DB`,`INFO` from `PROCESSLIST`')
procs = cur.fetchall()
found_ids = []
skipped = 0
for proc in procs:
if db_names and proc['DB'] not in db_names:
continue # we are not looking for this database
if proc['USER'] in rads.SYS_MYSQL_USERS:
skipped += 1 # restricted user
continue
if selects_only: # we only want SELECTS
query = str(proc['INFO']).upper() if proc['INFO'] else ''
if not query.lstrip().startswith('SELECT'):
continue # this is not a select
found_ids.append(proc['ID'])
if skipped:
print(f'Warning: skipped {skipped} queries from system users')
return found_ids
def main():
"""main: redirect to function based on parse_args result"""
func, targets = parse_args()
if func == 'kill':
return kill(targets)
if func == 'killdb':
return killdb(targets)
if func == 'killqueries':
return killqueries()
if func == 'killselects':
return killselects()
if func == 'sockets':
return sockets()
if func == 'active':
return active()
raise RuntimeError(f"{func=}")
if __name__ == '__main__':
try:
main()
except pymysql.Error as exc:
print(red(str(exc)), file=sys.stderr)
sys.exit(3)
Zerion Mini Shell 1.0