Mini Shell

Direktori : /proc/self/root/opt/sharedrads/
Upload File :
Current File : //proc/self/root/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