Отсев баз через source.exclude_databases

list_databases() прячит базы по массиву строк: подстрока без регистра
или маска со * ? [. Фильтр — единственное место рождения списка, поэтому
одинаков для меню, пульта и list; явное имя в CLI не трогает.
This commit is contained in:
2026-08-20 06:02:55 +03:00
parent 9404d1dd08
commit 975934373b
5 changed files with 65 additions and 1 deletions
+3
View File
@@ -75,6 +75,9 @@ matches(text, query) ; общий фильтр для pick() и обоих с
`kind` (`dump` или `sync`), а правки полей уезжают в `OpSpec.overrides` и накладываются `kind` (`dump` или `sync`), а правки полей уезжают в `OpSpec.overrides` и накладываются
копией конфига в `op_cfg()`. Файл `config.json` пульт не переписывает, значения живут копией конфига в `op_cfg()`. Файл `config.json` пульт не переписывает, значения живут
до выхода в `DashboardScreen.op_defaults`. до выхода в `DashboardScreen.op_defaults`.
- `source.exclude_databases` отсеивается в `list_databases()` — единственном месте, где
список рождается, поэтому фильтр одинаков для меню, пульта и `list`. Явное имя базы в CLI
через список не проходит и потому не фильтруется.
- Имя базы — параметр, а не поле конфига. `source` описывает только сервер - Имя базы — параметр, а не поле конфига. `source` описывает только сервер
(`maintenance_database` — куда подключаться, чтобы прочитать `pg_database`), (`maintenance_database` — куда подключаться, чтобы прочитать `pg_database`),
`target.database: null` означает «локально как на стенде»; переопределяется `--target-db`. `target.database: null` означает «локально как на стенде»; переопределяется `--target-db`.
+8
View File
@@ -142,6 +142,7 @@ python pg_stand_sync.py restore dumps/zpas-20260820-031332.dump
| `dump_dir` | `dumps` | куда складывать дампы (относительный путь — от каталога скрипта) | | `dump_dir` | `dumps` | куда складывать дампы (относительный путь — от каталога скрипта) |
| `keep_dumps` | `5` | сколько последних дампов хранить, `0` — не чистить | | `keep_dumps` | `5` | сколько последних дампов хранить, `0` — не чистить |
| `source.maintenance_database` | `postgres` | база, к которой подключаться для чтения списка баз | | `source.maintenance_database` | `postgres` | база, к которой подключаться для чтения списка баз |
| `source.exclude_databases` | `[]` | массив строк: базы, которые не показывать в списках |
| `source.password_env` | — | имя переменной окружения с паролем вместо `password` в файле | | `source.password_env` | — | имя переменной окружения с паролем вместо `password` в файле |
| `target.database` | `null` | имя локальной базы; `null` — как на стенде | | `target.database` | `null` | имя локальной базы; `null` — как на стенде |
| `target.recreate` | `true` | DROP + CREATE локальной базы перед восстановлением | | `target.recreate` | `true` | DROP + CREATE локальной базы перед восстановлением |
@@ -150,6 +151,13 @@ python pg_stand_sync.py restore dumps/zpas-20260820-031332.dump
| `restore.exit_on_error` | `false` | падать на первой ошибке восстановления | | `restore.exit_on_error` | `false` | падать на первой ошибке восстановления |
| `post_restore_sql` | `[]` | список SQL, выполняемых в целевой базе после восстановления | | `post_restore_sql` | `[]` | список SQL, выполняемых в целевой базе после восстановления |
```json
"exclude_databases": ["_old", "keycloak", "*_tmp"]
```
Строка без спецсимволов — подстрока (регистр не важен), со `*`, `?` или `[` — маска.
Фильтр действует только на списки: базу, названную прямо (`sync zpas_old`), он не прячет.
Пароль ищется в три шага: `password` в конфиге → переменная окружения из `password_env` Пароль ищется в три шага: `password` в конфиге → переменная окружения из `password_env`
запрос с терминала (один раз за запуск, ввод скрыт). В неинтерактивном запуске третьего шага запрос с терминала (один раз за запуск, ввод скрыт). В неинтерактивном запуске третьего шага
нет — либо `password_env`, либо `pgpass.conf`. Пароль всегда уходит в дочерний процесс через нет — либо `password_env`, либо `pgpass.conf`. Пароль всегда уходит в дочерний процесс через
+1
View File
@@ -7,6 +7,7 @@
"host": "postgresql.lan", "host": "postgresql.lan",
"port": 5432, "port": 5432,
"maintenance_database": "postgres", "maintenance_database": "postgres",
"exclude_databases": [],
"user": "postgres", "user": "postgres",
"password": null, "password": null,
"password_env": "STAND_PGPASSWORD", "password_env": "STAND_PGPASSWORD",
+24 -1
View File
@@ -7,6 +7,7 @@
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import fnmatch
import getpass import getpass
import importlib.util import importlib.util
import json import json
@@ -358,8 +359,28 @@ def psql_exec(tools: dict, node: dict, dbname: str, sql: str, what: str,
run(cmd, env, what, dry_run) run(cmd, env, what, dry_run)
def excluded(name: str, patterns) -> bool:
"""Прячется ли база фильтром: подстрока без регистра, а со звёздочкой — маска."""
low = name.lower()
for raw in patterns or []:
pattern = str(raw).strip().lower()
if not pattern:
continue
if any(ch in pattern for ch in "*?["):
if fnmatch.fnmatch(low, pattern):
return True
elif pattern in low:
return True
return False
def list_databases(tools: dict, node: dict, label: str) -> list: def list_databases(tools: dict, node: dict, label: str) -> list:
"""Список баз сервера: (имя, человекочитаемый размер).""" """Список баз сервера: (имя, человекочитаемый размер).
Отсев по node.exclude_databases делается здесь, в единственном месте, где список
вообще рождается: и меню, и пульт, и `list` получают его уже без мусорных баз.
Явное имя базы в CLI фильтр не трогает — оно списком не ходит.
"""
admin_db = node.get("maintenance_database") or node.get("database") or "postgres" admin_db = node.get("maintenance_database") or node.get("database") or "postgres"
env = conn_env(node, label, admin_db) env = conn_env(node, label, admin_db)
sql = ( sql = (
@@ -375,6 +396,8 @@ def list_databases(tools: dict, node: dict, label: str) -> list:
if not line.strip(): if not line.strip():
continue continue
name, _, size = line.partition("\t") name, _, size = line.partition("\t")
if excluded(name, node.get("exclude_databases")):
continue
rows.append((name, size or "")) rows.append((name, size or ""))
return rows return rows
+29
View File
@@ -1034,3 +1034,32 @@ def test_arrows_walk_dialog_fields(cfg, monkeypatch):
await pilot.press("escape") await pilot.press("escape")
await pilot.pause() await pilot.pause()
asyncio.run(scenario()) asyncio.run(scenario())
def test_exclude_databases_hides_rows(monkeypatch):
"""exclude_databases — массив строк: подстрока без регистра, со звёздочкой — маска."""
assert core.excluded("zpas_test", ["test"])
assert core.excluded("ZPAS_TEST", ["test"]) # регистр не важен
assert core.excluded("zpas_old_2", ["*_old_?"]) # маска
assert not core.excluded("zpas", ["test", "*_old"])
assert not core.excluded("zpas", []) # пустой список ничего не прячет
assert not core.excluded("zpas", ["", " "]) # и пустые строки тоже
node = {"host": "stand", "user": "postgres", "password": "x",
"maintenance_database": "postgres",
"exclude_databases": ["_old", "keycloak", "*_tmp"]}
monkeypatch.setattr(core, "capture", lambda cmd, env, what:
"zpas\t1284 MB\nzpas_old\t312 MB\nkeycloak\t12 MB\n"
"media\t8 MB\nbuild_tmp\t1 MB\n")
rows = core.list_databases({"psql": "psql"}, node, "стенд")
assert [name for name, _ in rows] == ["zpas", "media"]
def test_exclude_databases_does_not_touch_explicit_name(cfg, monkeypatch):
"""Скрытая из списка база остаётся доступной, если имя названо прямо в CLI."""
cfg["source"]["exclude_databases"] = ["zpas"]
called = []
monkeypatch.setattr(core, "dump", lambda cfg_, tools, dbname, out, dry:
called.append(dbname) or out)
core.do_dump(cfg, {"pg_dump": "pg_dump"}, ["zpas"], dry_run=True)
assert called == ["zpas"]