feat: multi-type server support — SQL, Redis, Grafana, Prometheus, Telnet, WinRM, RDP/VNC

Full implementation of multi-type server management across GUI and CLI:

New clients: SQLClient (MariaDB/MSSQL/PostgreSQL), RedisClient, GrafanaClient,
PrometheusClient, TelnetSession, WinRMClient, RemoteDesktopLauncher.

New GUI tabs: QueryTab (SQL editor + Treeview), RedisTab (console + history),
GrafanaTab (dashboards + alerts), PrometheusTab (PromQL + targets),
PowershellTab (PS/CMD), LaunchTab (RDP/VNC external client).

Infrastructure: TAB_REGISTRY for conditional tabs per server type,
adaptive server_dialog fields, colored type badges in sidebar,
status checker for all types (SSH/TCP/SQL/Redis/HTTP), 100+ i18n keys.

CLI: ssh.py extended with --sql, --redis, --grafana-*, --prom-*, --ps, --cmd.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chrome-storm-c442
2026-02-24 09:35:24 -05:00
parent 2d1d942ddc
commit eede67e6a9
26 changed files with 3990 additions and 168 deletions

View File

@@ -3,7 +3,7 @@
SSH utility for Claude Code — connects to servers by alias.
Credentials stored locally in servers.json (encrypted), NEVER exposed to AI API.
Usage:
Usage (SSH):
python ssh.py ALIAS "command" # run as configured user (auto-sudo if needed)
python ssh.py ALIAS --no-sudo "command" # run without sudo elevation
python ssh.py ALIAS --upload LOCAL REMOTE
@@ -16,6 +16,29 @@ Usage:
python ssh.py --set-note ALIAS "desc" # update server notes
python ssh.py --add ALIAS IP PORT USER PASSWORD [--note "desc"]
python ssh.py --remove ALIAS
SQL (type: mariadb / mssql / postgresql):
python ssh.py --sql ALIAS "SELECT * FROM users" # execute SQL query
python ssh.py --sql-databases ALIAS # list databases
python ssh.py --sql-tables ALIAS [database] # list tables
Redis (type: redis):
python ssh.py --redis ALIAS "GET mykey" # execute Redis command
python ssh.py --redis-info ALIAS # Redis INFO
python ssh.py --redis-keys ALIAS "user:*" # SCAN keys by pattern
Grafana (type: grafana):
python ssh.py --grafana-dashboards ALIAS # list dashboards
python ssh.py --grafana-alerts ALIAS # list alerts
Prometheus (type: prometheus):
python ssh.py --prom-query ALIAS "up" # execute PromQL query
python ssh.py --prom-targets ALIAS # list targets
python ssh.py --prom-alerts ALIAS # list alerts
WinRM (type: winrm):
python ssh.py --ps ALIAS "Get-Process" # PowerShell via WinRM
python ssh.py --cmd ALIAS "dir" # CMD via WinRM
"""
import sys
@@ -464,6 +487,415 @@ def remove_from_ssh_config(alias):
f.writelines(new_lines)
# ── SQL commands ──────────────────────────────────────
def _print_table(headers: list, rows: list):
"""Print a formatted ASCII table."""
if not rows:
print("(no rows)")
return
widths = [len(str(h)) for h in headers]
for row in rows:
for i, val in enumerate(row):
widths[i] = max(widths[i], len(str(val)))
fmt = " ".join(f"{{:<{w}}}" for w in widths)
print(fmt.format(*headers))
print(" ".join("-" * w for w in widths))
for row in rows:
print(fmt.format(*[str(v) for v in row]))
def run_sql(server: dict, query: str):
"""Execute SQL query against mariadb/mssql/postgresql server."""
stype = server.get("type", "mariadb")
host = server["ip"]
port = server.get("port", 3306)
user = server.get("user", "root")
password = server.get("password", "")
database = server.get("database", "")
if stype in ("mariadb", "mysql"):
import pymysql
conn = pymysql.connect(host=host, port=port, user=user, password=password,
database=database or None, connect_timeout=15,
charset="utf8mb4", cursorclass=pymysql.cursors.Cursor)
elif stype == "mssql":
import pymssql
conn = pymssql.connect(server=host, port=port, user=user, password=password,
database=database or None, login_timeout=15)
elif stype == "postgresql":
import psycopg2
port = server.get("port", 5432)
conn = psycopg2.connect(host=host, port=port, user=user, password=password,
dbname=database or None, connect_timeout=15)
else:
print(f"ERROR: Unsupported SQL type '{stype}'. Use mariadb, mssql, or postgresql.")
sys.exit(1)
try:
cur = conn.cursor()
cur.execute(query)
if cur.description:
headers = [desc[0] for desc in cur.description]
rows = cur.fetchall()
_print_table(headers, rows)
print(f"\n({len(rows)} row{'s' if len(rows) != 1 else ''})")
else:
conn.commit()
affected = cur.rowcount
print(f"OK: {affected} row{'s' if affected != 1 else ''} affected")
cur.close()
finally:
conn.close()
def sql_databases(server: dict):
"""List databases on SQL server."""
stype = server.get("type", "mariadb")
if stype in ("mariadb", "mysql"):
run_sql(server, "SHOW DATABASES")
elif stype == "mssql":
run_sql(server, "SELECT name FROM sys.databases ORDER BY name")
elif stype == "postgresql":
run_sql(server, "SELECT datname AS database FROM pg_database WHERE datistemplate = false ORDER BY datname")
else:
print(f"ERROR: Unsupported SQL type '{stype}'.")
sys.exit(1)
def sql_tables(server: dict, database: str = None):
"""List tables on SQL server, optionally for a specific database."""
stype = server.get("type", "mariadb")
if database:
server = dict(server)
server["database"] = database
if stype in ("mariadb", "mysql"):
if database:
run_sql(server, f"SHOW TABLES FROM `{database}`")
else:
run_sql(server, "SHOW TABLES")
elif stype == "mssql":
run_sql(server, "SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE FROM INFORMATION_SCHEMA.TABLES ORDER BY TABLE_SCHEMA, TABLE_NAME")
elif stype == "postgresql":
run_sql(server, "SELECT schemaname, tablename FROM pg_tables WHERE schemaname NOT IN ('pg_catalog', 'information_schema') ORDER BY schemaname, tablename")
else:
print(f"ERROR: Unsupported SQL type '{stype}'.")
sys.exit(1)
# ── Redis commands ────────────────────────────────────
def run_redis_cmd(server: dict, command: str):
"""Execute a Redis command."""
import redis as redis_lib
host = server["ip"]
port = server.get("port", 6379)
password = server.get("password", "") or None
db_index = server.get("db_index", 0)
ssl_enabled = server.get("ssl", False)
r = redis_lib.Redis(host=host, port=port, password=password, db=db_index,
decode_responses=True, socket_timeout=10, ssl=ssl_enabled)
try:
parts = command.split()
if not parts:
print("ERROR: Empty Redis command")
sys.exit(1)
result = r.execute_command(*parts)
if isinstance(result, list):
for i, item in enumerate(result):
print(f"{i + 1}) {item}")
print(f"\n({len(result)} items)")
elif isinstance(result, dict):
for k, v in result.items():
print(f"{k}: {v}")
elif isinstance(result, bytes):
print(result.decode("utf-8", errors="replace"))
else:
print(result)
finally:
r.close()
def redis_info(server: dict):
"""Show Redis INFO."""
import redis as redis_lib
host = server["ip"]
port = server.get("port", 6379)
password = server.get("password", "") or None
db_index = server.get("db_index", 0)
ssl_enabled = server.get("ssl", False)
r = redis_lib.Redis(host=host, port=port, password=password, db=db_index,
decode_responses=True, socket_timeout=10, ssl=ssl_enabled)
try:
info = r.info()
# Print key sections
sections = ["redis_version", "redis_mode", "os", "uptime_in_seconds",
"connected_clients", "used_memory_human", "used_memory_peak_human",
"total_connections_received", "total_commands_processed",
"keyspace_hits", "keyspace_misses", "role"]
print(f"{'Key':<35} {'Value'}")
print("-" * 60)
for key in sections:
if key in info:
print(f"{key:<35} {info[key]}")
# Print keyspace info (db0, db1, etc.)
for key in sorted(info.keys()):
if key.startswith("db"):
print(f"{key:<35} {info[key]}")
finally:
r.close()
def redis_keys(server: dict, pattern: str):
"""SCAN keys matching a pattern."""
import redis as redis_lib
host = server["ip"]
port = server.get("port", 6379)
password = server.get("password", "") or None
db_index = server.get("db_index", 0)
ssl_enabled = server.get("ssl", False)
r = redis_lib.Redis(host=host, port=port, password=password, db=db_index,
decode_responses=True, socket_timeout=10, ssl=ssl_enabled)
try:
keys = []
cursor = 0
while True:
cursor, batch = r.scan(cursor=cursor, match=pattern, count=200)
keys.extend(batch)
if cursor == 0:
break
if len(keys) >= 1000:
print("(truncated at 1000 keys)")
break
keys.sort()
for k in keys:
print(k)
print(f"\n({len(keys)} key{'s' if len(keys) != 1 else ''})")
finally:
r.close()
# ── Grafana commands ──────────────────────────────────
def _grafana_request(server: dict, endpoint: str) -> dict:
"""Make an authenticated GET request to Grafana API."""
import requests
host = server["ip"]
port = server.get("port", 3000)
protocol = "https" if server.get("ssl", False) else "http"
base_url = server.get("base_url", f"{protocol}://{host}:{port}")
api_key = server.get("api_key", server.get("password", ""))
headers = {}
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
url = f"{base_url.rstrip('/')}/api/{endpoint.lstrip('/')}"
resp = requests.get(url, headers=headers, timeout=15, verify=server.get("ssl_verify", True))
resp.raise_for_status()
return resp.json()
def grafana_dashboards(server: dict):
"""List Grafana dashboards."""
data = _grafana_request(server, "search?type=dash-db")
if not data:
print("(no dashboards found)")
return
headers = ["UID", "Title", "Folder", "URL"]
rows = []
for d in data:
rows.append([
d.get("uid", ""),
d.get("title", ""),
d.get("folderTitle", "(root)"),
d.get("url", ""),
])
_print_table(headers, rows)
print(f"\n({len(rows)} dashboard{'s' if len(rows) != 1 else ''})")
def grafana_alerts(server: dict):
"""List Grafana alert rules."""
data = _grafana_request(server, "alertmanager/grafana/api/v2/alerts")
if not data:
print("(no alerts)")
return
headers = ["Status", "Name", "Severity", "Summary"]
rows = []
for alert in data:
status = alert.get("status", {}).get("state", "unknown")
labels = alert.get("labels", {})
annotations = alert.get("annotations", {})
rows.append([
status,
labels.get("alertname", ""),
labels.get("severity", ""),
annotations.get("summary", "")[:80],
])
_print_table(headers, rows)
print(f"\n({len(rows)} alert{'s' if len(rows) != 1 else ''})")
# ── Prometheus commands ───────────────────────────────
def _prom_request(server: dict, endpoint: str, params: dict = None) -> dict:
"""Make a GET request to Prometheus API."""
import requests
host = server["ip"]
port = server.get("port", 9090)
protocol = "https" if server.get("ssl", False) else "http"
base_url = server.get("base_url", f"{protocol}://{host}:{port}")
auth = None
user = server.get("user", "")
password = server.get("password", "")
if user and password:
auth = (user, password)
url = f"{base_url.rstrip('/')}/api/v1/{endpoint.lstrip('/')}"
resp = requests.get(url, params=params, auth=auth, timeout=15,
verify=server.get("ssl_verify", True))
resp.raise_for_status()
return resp.json()
def prom_query(server: dict, query: str):
"""Execute a PromQL instant query."""
data = _prom_request(server, "query", {"query": query})
status = data.get("status", "")
if status != "success":
print(f"ERROR: Prometheus returned status '{status}'")
if "error" in data:
print(f" {data['error']}")
sys.exit(1)
result = data.get("data", {})
result_type = result.get("resultType", "")
results = result.get("result", [])
if not results:
print("(no results)")
return
if result_type == "vector":
headers = ["Metric", "Value", "Timestamp"]
rows = []
for r in results:
metric = r.get("metric", {})
label_str = ", ".join(f'{k}="{v}"' for k, v in metric.items())
ts, val = r.get("value", [0, ""])
rows.append([label_str or "{}", val, ts])
_print_table(headers, rows)
elif result_type == "scalar":
ts, val = results
print(f"Scalar: {val} (at {ts})")
elif result_type == "string":
ts, val = results
print(f"String: {val} (at {ts})")
elif result_type == "matrix":
for series in results:
metric = series.get("metric", {})
label_str = ", ".join(f'{k}="{v}"' for k, v in metric.items())
print(f"\n--- {label_str or '{}'} ---")
values = series.get("values", [])
for ts, val in values[-20:]: # last 20 samples
print(f" [{ts}] {val}")
if len(values) > 20:
print(f" ... ({len(values)} total samples, showing last 20)")
print(f"\n({len(results)} result{'s' if len(results) != 1 else ''}, type: {result_type})")
def prom_targets(server: dict):
"""List Prometheus scrape targets."""
data = _prom_request(server, "targets")
active = data.get("data", {}).get("activeTargets", [])
if not active:
print("(no active targets)")
return
headers = ["Job", "Instance", "State", "Health", "Last Scrape"]
rows = []
for t in active:
labels = t.get("labels", {})
rows.append([
labels.get("job", ""),
labels.get("instance", ""),
t.get("scrapePool", ""),
t.get("health", ""),
t.get("lastScrape", "")[:19],
])
_print_table(headers, rows)
print(f"\n({len(rows)} target{'s' if len(rows) != 1 else ''})")
def prom_alerts(server: dict):
"""List Prometheus alerts."""
data = _prom_request(server, "alerts")
alerts = data.get("data", {}).get("alerts", [])
if not alerts:
print("(no alerts)")
return
headers = ["State", "Name", "Severity", "Active Since"]
rows = []
for a in alerts:
labels = a.get("labels", {})
rows.append([
a.get("state", ""),
labels.get("alertname", ""),
labels.get("severity", ""),
a.get("activeAt", "")[:19],
])
_print_table(headers, rows)
print(f"\n({len(rows)} alert{'s' if len(rows) != 1 else ''})")
# ── WinRM commands ────────────────────────────────────
def _get_winrm_session(server: dict):
"""Create a WinRM session."""
import winrm
host = server["ip"]
port = server.get("port", 5985)
user = server.get("user", "Administrator")
password = server.get("password", "")
protocol = "https" if server.get("ssl", False) or port == 5986 else "http"
transport = server.get("transport", "ntlm")
endpoint = f"{protocol}://{host}:{port}/wsman"
session = winrm.Session(endpoint, auth=(user, password), transport=transport,
server_cert_validation="ignore" if protocol == "https" else "validate")
return session
def run_winrm_ps(server: dict, command: str):
"""Execute PowerShell command via WinRM."""
session = _get_winrm_session(server)
result = session.run_ps(command)
out = result.std_out.decode("utf-8", errors="replace").strip()
err = result.std_err.decode("utf-8", errors="replace").strip()
if out:
print(out)
if err:
print(err, file=sys.stderr)
sys.exit(result.status_code)
def run_winrm_cmd(server: dict, command: str):
"""Execute CMD command via WinRM."""
session = _get_winrm_session(server)
result = session.run_cmd(command)
out = result.std_out.decode("utf-8", errors="replace").strip()
err = result.std_err.decode("utf-8", errors="replace").strip()
if out:
print(out)
if err:
print(err, file=sys.stderr)
sys.exit(result.status_code)
# ── Main ──────────────────────────────────────────────
def main():
@@ -488,6 +920,80 @@ def main():
if cmd == "--remove" and len(sys.argv) >= 3:
remove_server(sys.argv[2]); sys.exit(0)
# ── SQL commands (global-style: --sql ALIAS ...) ──
if cmd == "--sql" and len(sys.argv) >= 4:
_, servers = load_servers()
alias = _resolve_alias(sys.argv[2], servers)
run_sql(servers[alias], sys.argv[3])
sys.exit(0)
if cmd == "--sql-databases" and len(sys.argv) >= 3:
_, servers = load_servers()
alias = _resolve_alias(sys.argv[2], servers)
sql_databases(servers[alias])
sys.exit(0)
if cmd == "--sql-tables" and len(sys.argv) >= 3:
_, servers = load_servers()
alias = _resolve_alias(sys.argv[2], servers)
db = sys.argv[3] if len(sys.argv) >= 4 else None
sql_tables(servers[alias], db)
sys.exit(0)
# ── Redis commands ──
if cmd == "--redis" and len(sys.argv) >= 4:
_, servers = load_servers()
alias = _resolve_alias(sys.argv[2], servers)
run_redis_cmd(servers[alias], sys.argv[3])
sys.exit(0)
if cmd == "--redis-info" and len(sys.argv) >= 3:
_, servers = load_servers()
alias = _resolve_alias(sys.argv[2], servers)
redis_info(servers[alias])
sys.exit(0)
if cmd == "--redis-keys" and len(sys.argv) >= 4:
_, servers = load_servers()
alias = _resolve_alias(sys.argv[2], servers)
redis_keys(servers[alias], sys.argv[3])
sys.exit(0)
# ── Grafana commands ──
if cmd == "--grafana-dashboards" and len(sys.argv) >= 3:
_, servers = load_servers()
alias = _resolve_alias(sys.argv[2], servers)
grafana_dashboards(servers[alias])
sys.exit(0)
if cmd == "--grafana-alerts" and len(sys.argv) >= 3:
_, servers = load_servers()
alias = _resolve_alias(sys.argv[2], servers)
grafana_alerts(servers[alias])
sys.exit(0)
# ── Prometheus commands ──
if cmd == "--prom-query" and len(sys.argv) >= 4:
_, servers = load_servers()
alias = _resolve_alias(sys.argv[2], servers)
prom_query(servers[alias], sys.argv[3])
sys.exit(0)
if cmd == "--prom-targets" and len(sys.argv) >= 3:
_, servers = load_servers()
alias = _resolve_alias(sys.argv[2], servers)
prom_targets(servers[alias])
sys.exit(0)
if cmd == "--prom-alerts" and len(sys.argv) >= 3:
_, servers = load_servers()
alias = _resolve_alias(sys.argv[2], servers)
prom_alerts(servers[alias])
sys.exit(0)
# ── WinRM commands ──
if cmd == "--ps" and len(sys.argv) >= 4:
_, servers = load_servers()
alias = _resolve_alias(sys.argv[2], servers)
run_winrm_ps(servers[alias], sys.argv[3])
if cmd == "--cmd" and len(sys.argv) >= 4:
_, servers = load_servers()
alias = _resolve_alias(sys.argv[2], servers)
run_winrm_cmd(servers[alias], sys.argv[3])
# Server commands — exact match first, then fuzzy search by keyword
alias = cmd
_, servers = load_servers()