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:
508
tools/ssh.py
508
tools/ssh.py
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user