Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9a81a4825 | ||
|
|
3bafb0deb8 | ||
|
|
b37e696094 | ||
|
|
289ce65431 | ||
|
|
704ce3bef2 |
@@ -26,7 +26,7 @@ ServerManager — **кроссплатформенное** Desktop GUI (CustomTk
|
||||
| grafana | `grafana_client.py` (requests) | Dashboards, Info, Setup | `--grafana-dashboards`, `--grafana-alerts` |
|
||||
| prometheus | `prometheus_client.py` (requests) | Metrics, Info, Setup | `--prom-query`, `--prom-targets`, `--prom-alerts` |
|
||||
| winrm | `winrm_client.py` (pywinrm) | PowerShell, Info, Setup | `--ps`, `--cmd` |
|
||||
| s3 | `s3_client.py` (boto3) | Objects, Info, Setup | `--s3-buckets`, `--s3-ls`, `--s3-upload`, `--s3-download`, `--s3-delete` |
|
||||
| s3 | `s3_client.py` (boto3) | Objects, Info, Setup | `--s3-buckets`, `--s3-ls`, `--s3-upload`, `--s3-download`, `--s3-delete`, `--s3-url` |
|
||||
| rdp/vnc | `remote_desktop.py` | Launch, Info, Setup | — (запуск внешнего клиента) |
|
||||
|
||||
## БЕЗОПАСНОСТЬ
|
||||
@@ -139,6 +139,13 @@ tools/
|
||||
/ssh --redis ALIAS "GET key" # Redis-команда
|
||||
/ssh --redis-info ALIAS # Redis INFO
|
||||
/ssh --redis-keys ALIAS "pattern" # SCAN ключей
|
||||
# S3 / MinIO
|
||||
/ssh --s3-buckets ALIAS # Список бакетов
|
||||
/ssh --s3-ls ALIAS bucket[/prefix] # Список объектов
|
||||
/ssh --s3-upload ALIAS local bucket/key # Upload файла
|
||||
/ssh --s3-download ALIAS bucket/key local # Download файла
|
||||
/ssh --s3-delete ALIAS bucket/key # Удалить объект
|
||||
/ssh --s3-url ALIAS bucket/key [SEC] # Presigned URL (по умолчанию 1 час)
|
||||
# Grafana / Prometheus
|
||||
/ssh --grafana-dashboards ALIAS # Дашборды
|
||||
/ssh --prom-query ALIAS "up" # PromQL
|
||||
|
||||
70
build.py
70
build.py
@@ -182,9 +182,8 @@ def build():
|
||||
# Auto-deploy: sync shared files so Claude Code always has the latest
|
||||
deploy_shared_files()
|
||||
|
||||
# Publish release to Gitea + cleanup old remote releases
|
||||
# Publish release to Gitea
|
||||
publish_gitea_release(dst)
|
||||
cleanup_gitea_releases()
|
||||
|
||||
|
||||
def _get_gitea_auth() -> dict:
|
||||
@@ -312,60 +311,6 @@ def _version_key(path: str):
|
||||
return (0, 0, 0)
|
||||
|
||||
|
||||
def _tag_version_key(tag_name: str):
|
||||
"""Extract (major, minor, patch) from tag like 'v1.9.5'."""
|
||||
m = re.match(r'v(\d+)\.(\d+)\.(\d+)', tag_name)
|
||||
if m:
|
||||
return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
|
||||
return (0, 0, 0)
|
||||
|
||||
|
||||
def cleanup_gitea_releases():
|
||||
"""Keep the first release (v1.0.0) and the last 5 releases on Gitea, delete the rest."""
|
||||
auth = _get_gitea_auth()
|
||||
if not auth:
|
||||
return
|
||||
|
||||
# List all releases
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
f"{_GITEA_API}/releases?limit=50",
|
||||
headers=auth,
|
||||
)
|
||||
resp = urllib.request.urlopen(req, timeout=30)
|
||||
releases = json.loads(resp.read())
|
||||
except Exception as e:
|
||||
print(f"Gitea release list failed: {e}")
|
||||
return
|
||||
|
||||
if len(releases) <= 6:
|
||||
return
|
||||
|
||||
# Sort by semver
|
||||
releases.sort(key=lambda r: _tag_version_key(r.get("tag_name", "")))
|
||||
|
||||
first = releases[0]
|
||||
last_5 = releases[-5:]
|
||||
keep_ids = {first["id"]} | {r["id"] for r in last_5}
|
||||
|
||||
removed = []
|
||||
for r in releases:
|
||||
if r["id"] in keep_ids:
|
||||
continue
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
f"{_GITEA_API}/releases/{r['id']}",
|
||||
headers=auth,
|
||||
method="DELETE",
|
||||
)
|
||||
urllib.request.urlopen(req, timeout=15)
|
||||
removed.append(r.get("tag_name", "?"))
|
||||
except Exception as e:
|
||||
print(f"Failed to delete Gitea release {r.get('tag_name')}: {e}")
|
||||
|
||||
if removed:
|
||||
print(f"Cleaned {len(removed)} old Gitea releases: {', '.join(removed)}")
|
||||
|
||||
|
||||
def cleanup_old_releases():
|
||||
"""Keep the first release (v1.0.0) and the last 5 releases, delete the rest."""
|
||||
@@ -383,9 +328,20 @@ def cleanup_old_releases():
|
||||
keep = set([first] + last_5)
|
||||
|
||||
removed = []
|
||||
_flags = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
|
||||
for f in all_exes:
|
||||
if f not in keep:
|
||||
os.remove(f)
|
||||
# Use git rm so deletion is staged for commit
|
||||
try:
|
||||
subprocess.run(
|
||||
["git", "rm", "-f", "--quiet", f],
|
||||
cwd=PROJECT_DIR, creationflags=_flags,
|
||||
capture_output=True,
|
||||
)
|
||||
except Exception:
|
||||
# Fallback: just delete the file
|
||||
if os.path.exists(f):
|
||||
os.remove(f)
|
||||
removed.append(os.path.basename(f))
|
||||
|
||||
if removed:
|
||||
|
||||
@@ -33,6 +33,12 @@ class GroupDialog(ctk.CTkToplevel):
|
||||
self.resizable(False, False)
|
||||
self.transient(master)
|
||||
self.grab_set()
|
||||
self.protocol("WM_DELETE_WINDOW", self._on_close)
|
||||
|
||||
# Fix: restore dialog when parent is un-minimized
|
||||
self._master_ref = master
|
||||
master.bind("<Map>", self._on_parent_map, add="+")
|
||||
self.bind("<Unmap>", self._on_unmap, add="+")
|
||||
|
||||
# ── Name ──
|
||||
ctk.CTkLabel(self, text=t("group_name"), anchor="w").pack(
|
||||
@@ -71,7 +77,7 @@ class GroupDialog(ctk.CTkToplevel):
|
||||
btn_frame.pack(fill="x", padx=20, pady=(15, 10))
|
||||
|
||||
ctk.CTkButton(btn_frame, text=t("cancel"), width=80,
|
||||
fg_color="gray", command=self.destroy).pack(side="left")
|
||||
fg_color="gray", command=self._on_close).pack(side="left")
|
||||
ctk.CTkButton(btn_frame, text=t("save"), width=80,
|
||||
command=self._save).pack(side="right")
|
||||
|
||||
@@ -90,6 +96,34 @@ class GroupDialog(ctk.CTkToplevel):
|
||||
else:
|
||||
btn.configure(border_color=fg)
|
||||
|
||||
def _on_parent_map(self, event=None):
|
||||
try:
|
||||
if not self.winfo_exists():
|
||||
return
|
||||
self.deiconify()
|
||||
self.lift()
|
||||
self.focus_force()
|
||||
self.grab_set()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _on_unmap(self, event=None):
|
||||
try:
|
||||
self.grab_release()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _on_close(self):
|
||||
try:
|
||||
self._master_ref.unbind("<Map>")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.grab_release()
|
||||
except Exception:
|
||||
pass
|
||||
self.destroy()
|
||||
|
||||
def _save(self):
|
||||
name = self._name_var.get().strip()
|
||||
if not name:
|
||||
@@ -107,4 +141,4 @@ class GroupDialog(ctk.CTkToplevel):
|
||||
group = self.store.add_group(name, self._selected_color)
|
||||
self.result = group
|
||||
|
||||
self.destroy()
|
||||
self._on_close()
|
||||
|
||||
@@ -62,6 +62,11 @@ class ServerDialog(ctk.CTkToplevel):
|
||||
# Release grab on close (prevents stuck app)
|
||||
self.protocol("WM_DELETE_WINDOW", self._on_close)
|
||||
|
||||
# Fix: restore dialog when parent is un-minimized
|
||||
self._master_ref = master
|
||||
master.bind("<Map>", self._on_parent_map, add="+")
|
||||
self.bind("<Unmap>", self._on_unmap, add="+")
|
||||
|
||||
self._field_frames: dict[str, ctk.CTkFrame] = {}
|
||||
self._build_ui(server)
|
||||
|
||||
@@ -485,8 +490,31 @@ class ServerDialog(ctk.CTkToplevel):
|
||||
except ValueError as e:
|
||||
self._show_error(str(e))
|
||||
|
||||
def _on_parent_map(self, event=None):
|
||||
"""Force-restore dialog when parent window is un-minimized."""
|
||||
try:
|
||||
if not self.winfo_exists():
|
||||
return
|
||||
self.deiconify()
|
||||
self.lift()
|
||||
self.focus_force()
|
||||
self.grab_set()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _on_unmap(self, event=None):
|
||||
"""Release grab when dialog is minimized to prevent input lock."""
|
||||
try:
|
||||
self.grab_release()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _on_close(self):
|
||||
"""Release grab and destroy — prevents stuck app on minimize."""
|
||||
try:
|
||||
self._master_ref.unbind("<Map>")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.grab_release()
|
||||
except Exception:
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,7 +1,7 @@
|
||||
# Скилл /ssh — управление удалёнными серверами
|
||||
|
||||
Ты управляешь удалёнными серверами через универсальную CLI-утилиту.
|
||||
Поддерживаются: SSH, SQL (MariaDB/MSSQL/PostgreSQL), Redis, S3, Grafana, Prometheus, WinRM (PowerShell/CMD).
|
||||
Поддерживаются: SSH, SQL (MariaDB/MSSQL/PostgreSQL), Redis, S3/MinIO, Grafana, Prometheus, WinRM (PowerShell/CMD).
|
||||
|
||||
## ВАЖНО — Безопасность
|
||||
|
||||
@@ -19,33 +19,47 @@
|
||||
|
||||
Пользователь передаёт через `$ARGUMENTS`. Разбери и выполни.
|
||||
|
||||
## КРИТИЧНО — Команды зависят от типа сервера
|
||||
## КРИТИЧНО — СНАЧАЛА ПРОВЕРЬ ТИП СЕРВЕРА
|
||||
|
||||
`--list` возвращает колонку `Type` для каждого сервера. **Тип определяет какие команды использовать:**
|
||||
**ПЕРЕД ЛЮБОЙ операцией** с сервером — **ОБЯЗАТЕЛЬНО** выполни `--list` и посмотри колонку `Type`.
|
||||
**ЗАПРЕЩЕНО** угадывать тип сервера. MinIO/S3 — это НЕ SSH, Redis — это НЕ SSH, MariaDB — это НЕ SSH.
|
||||
|
||||
| Тип | Команды |
|
||||
|-----|---------|
|
||||
| `ssh` | `ALIAS "command"`, `--upload`, `--download`, `--ping`, `--install-key` |
|
||||
| `telnet` | `ALIAS "command"` (как ssh, но без SFTP/sudo/ключей) |
|
||||
| `mariadb` / `mssql` / `postgresql` | `--sql`, `--sql-databases`, `--sql-tables` |
|
||||
| `redis` | `--redis`, `--redis-info`, `--redis-keys` |
|
||||
| `s3` | `--s3-buckets`, `--s3-ls`, `--s3-upload`, `--s3-download`, `--s3-delete` |
|
||||
| `grafana` | `--grafana-dashboards`, `--grafana-alerts` |
|
||||
| `prometheus` | `--prom-query`, `--prom-targets`, `--prom-alerts` |
|
||||
| `winrm` | `--ps`, `--cmd` |
|
||||
| `rdp` / `vnc` | Только GUI (запуск внешнего клиента), CLI-команд нет |
|
||||
**Тип сервера определяет КАКИЕ команды использовать. Использование команд не того типа — СЛОМАЕТ операцию.**
|
||||
|
||||
**`ALIAS "command"` — ТОЛЬКО для типа `ssh`.** Для Redis — `--redis`, для SQL — `--sql`, для WinRM — `--ps`/`--cmd` и т.д.
|
||||
| Тип | Команды | НЕ использовать |
|
||||
|-----|---------|-----------------|
|
||||
| `ssh` | `ALIAS "command"`, `--upload`, `--download`, `--ping`, `--install-key` | — |
|
||||
| `telnet` | `ALIAS "command"` (без SFTP/sudo/ключей) | `--upload`, `--download` |
|
||||
| `mariadb` / `mssql` / `postgresql` | `--sql`, `--sql-databases`, `--sql-tables` | `ALIAS "command"` |
|
||||
| `redis` | `--redis`, `--redis-info`, `--redis-keys` | `ALIAS "command"` |
|
||||
| `s3` (MinIO, AWS S3, и др.) | `--s3-buckets`, `--s3-ls`, `--s3-upload`, `--s3-download`, `--s3-delete`, `--s3-url` | `ALIAS "command"`, `--upload`, `--download` |
|
||||
| `grafana` | `--grafana-dashboards`, `--grafana-alerts` | `ALIAS "command"` |
|
||||
| `prometheus` | `--prom-query`, `--prom-targets`, `--prom-alerts` | `ALIAS "command"` |
|
||||
| `winrm` | `--ps`, `--cmd` | `ALIAS "command"` |
|
||||
| `rdp` / `vnc` | Только GUI | всё |
|
||||
|
||||
**`ALIAS "command"` (shell-команды типа ls, cat, mkdir) — ТОЛЬКО для типов `ssh` и `telnet`.**
|
||||
|
||||
```bash
|
||||
# Тип redis → --redis-info, НЕ ALIAS "INFO"
|
||||
python ~/.server-connections/ssh.py --redis-info "Reddis main ovh"
|
||||
# ❌ НЕПРАВИЛЬНО — MinIO/S3 это НЕ SSH, нельзя выполнять shell-команды
|
||||
python ~/.server-connections/ssh.py "minio-alias" "ls /bucket"
|
||||
python ~/.server-connections/ssh.py "minio-alias" "mkdir /bucket/folder"
|
||||
|
||||
# Тип mariadb → --sql-databases, НЕ ALIAS "SHOW DATABASES"
|
||||
python ~/.server-connections/ssh.py --sql-databases "Maria Db Connection main ovh"
|
||||
# ✅ ПРАВИЛЬНО — S3-команды для типа s3
|
||||
python ~/.server-connections/ssh.py --s3-ls "minio-alias" bucket
|
||||
python ~/.server-connections/ssh.py --s3-upload "minio-alias" "D:/file.txt" bucket/folder/file.txt
|
||||
|
||||
# Тип ssh → ALIAS "command"
|
||||
python ~/.server-connections/ssh.py investor "uptime"
|
||||
# ❌ НЕПРАВИЛЬНО — Redis это НЕ SSH
|
||||
python ~/.server-connections/ssh.py "redis-alias" "INFO"
|
||||
|
||||
# ✅ ПРАВИЛЬНО
|
||||
python ~/.server-connections/ssh.py --redis-info "redis-alias"
|
||||
|
||||
# ❌ НЕПРАВИЛЬНО — MariaDB это НЕ SSH
|
||||
python ~/.server-connections/ssh.py "mariadb-alias" "SHOW DATABASES"
|
||||
|
||||
# ✅ ПРАВИЛЬНО
|
||||
python ~/.server-connections/ssh.py --sql-databases "mariadb-alias"
|
||||
```
|
||||
|
||||
## Общие команды
|
||||
@@ -159,7 +173,12 @@ python ~/.server-connections/ssh.py --redis-info ALIAS
|
||||
python ~/.server-connections/ssh.py --redis-keys ALIAS "user:*"
|
||||
```
|
||||
|
||||
## S3-команды (тип: s3)
|
||||
## S3-команды (тип: s3) — MinIO, AWS S3, любое S3-совместимое хранилище
|
||||
|
||||
**MinIO = тип `s3`.** Когда пользователь говорит "MinIO" или "S3" — используй ТОЛЬКО `--s3-*` команды.
|
||||
**НЕ пытайся** выполнять shell-команды (`ls`, `mkdir`, `cat`) на S3-серверах — это не SSH!
|
||||
|
||||
**Папки в S3 не существуют** — это префиксы. "Создать папку" = загрузить файл с префиксом в ключе (например `bucket/folder/file.txt`).
|
||||
|
||||
### Список бакетов
|
||||
```bash
|
||||
@@ -187,6 +206,25 @@ python ~/.server-connections/ssh.py --s3-download ALIAS bucket/key "D:/local/fil
|
||||
python ~/.server-connections/ssh.py --s3-delete ALIAS bucket/key
|
||||
```
|
||||
|
||||
### Получить ссылку на файл (presigned URL)
|
||||
```bash
|
||||
python ~/.server-connections/ssh.py --s3-url ALIAS bucket/key
|
||||
python ~/.server-connections/ssh.py --s3-url ALIAS bucket/key 86400
|
||||
```
|
||||
По умолчанию ссылка действует 1 час (3600 сек). Второй аргумент — время жизни в секундах (например 86400 = 24 часа).
|
||||
|
||||
### Типичный workflow: "создай папку и залей файл"
|
||||
```bash
|
||||
# 1. Посмотри бакеты
|
||||
python ~/.server-connections/ssh.py --s3-buckets ALIAS
|
||||
# 2. "Создать папку" = просто загрузить файл с нужным путём (prefix)
|
||||
python ~/.server-connections/ssh.py --s3-upload ALIAS "D:/file.txt" mybucket/newfolder/file.txt
|
||||
# 3. Проверить
|
||||
python ~/.server-connections/ssh.py --s3-ls ALIAS mybucket/newfolder/
|
||||
# 4. Получить ссылку
|
||||
python ~/.server-connections/ssh.py --s3-url ALIAS mybucket/newfolder/file.txt
|
||||
```
|
||||
|
||||
## Grafana-команды (тип: grafana)
|
||||
|
||||
### Список дашбордов
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Version info for ServerManager."""
|
||||
|
||||
__version__ = "1.9.11"
|
||||
__version__ = "1.9.14"
|
||||
__app_name__ = "ServerManager"
|
||||
__author__ = "aibot777"
|
||||
__description__ = "Desktop GUI for managing remote servers"
|
||||
|
||||
Reference in New Issue
Block a user