597 lines
19 KiB
Markdown
597 lines
19 KiB
Markdown
# Ostiary — удалённый запуск скриптов
|
||
|
||
Консольный клиент с текстовым интерфейсом (TUI) на Rust. Загружает иерархическое меню с сервера и позволяет запускать bash-скрипты, скачивать файлы и выполнять HTTP-запросы прямо из терминала. Все диалоги (выбор из списка, подтверждение, ввод текста) реализованы внутри интерфейса — никаких сырых `read -p` в терминале.
|
||
|
||
---
|
||
|
||
## Установка
|
||
|
||
### Быстрая установка (Linux / macOS)
|
||
|
||
```bash
|
||
curl -fsSL https://git.vainend.com/admin/ostiary/raw/branch/master/install.sh | bash
|
||
```
|
||
|
||
Скрипт определяет ОС и архитектуру, скачивает последний стабильный релиз и устанавливает бинарник в `~/.local/bin/ostiary`. Если этого пути нет в `$PATH` — автоматически добавляет строку в `~/.bashrc` (или `~/.bash_profile` / `~/.profile`). Не требует `sudo`.
|
||
|
||
### Ручная установка
|
||
|
||
Скачать нужный бинарник со страницы [Releases](https://git.vainend.com/admin/ostiary/releases):
|
||
|
||
| Файл | Платформа |
|
||
|---|---|
|
||
| `ostiary-linux-x86_64` | Linux x86-64 |
|
||
| `ostiary-linux-aarch64` | Linux ARM64 |
|
||
| `ostiary-macos-aarch64` | macOS Apple Silicon |
|
||
| `ostiary-macos-x86_64` | macOS Intel |
|
||
|
||
```bash
|
||
mkdir -p ~/.local/bin
|
||
chmod +x ostiary-linux-x86_64
|
||
mv ostiary-linux-x86_64 ~/.local/bin/ostiary
|
||
|
||
# Добавить в PATH если нужно
|
||
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
|
||
source ~/.bashrc
|
||
```
|
||
|
||
### Удаление
|
||
|
||
```bash
|
||
curl -fsSL https://git.vainend.com/admin/ostiary/raw/branch/master/uninstall.sh | bash
|
||
```
|
||
|
||
Или вручную:
|
||
|
||
```bash
|
||
rm ~/.local/bin/ostiary
|
||
rm -rf ~/.config/ostiary
|
||
```
|
||
|
||
### Переменная окружения
|
||
|
||
| Переменная | Описание |
|
||
|---|---|
|
||
| `OSTIARY_API` | Базовый URL Gitea API (если используете собственный инстанс) |
|
||
|
||
Пример с кастомным каталогом:
|
||
```bash
|
||
INSTALL_DIR=~/.local/bin bash install.sh
|
||
```
|
||
|
||
---
|
||
|
||
## Навигация
|
||
|
||
### Главное меню
|
||
|
||
| Клавиша | Действие |
|
||
|---|---|
|
||
| `↑` / `↓` или `k` / `j` | Перемещение по списку |
|
||
| `Enter` или `l` | Войти в категорию / запустить действие |
|
||
| `Esc` или `h` | Выйти из категории |
|
||
| `q` | Выйти из приложения |
|
||
| `R` | Перезагрузить меню с сервера |
|
||
|
||
### Popup запущенного скрипта
|
||
|
||
| Клавиша | Действие |
|
||
|---|---|
|
||
| `↑` / `↓` или `k` / `j` | Прокрутка лога / навигация по селектору |
|
||
| `PgUp` / `PgDn` | Прокрутка лога на 10 строк |
|
||
| `Y` / `N` или `l` / `h` | Ответ на подтверждение (без Enter) |
|
||
| `Enter` или `l` | Выбрать пункт селектора / подтвердить |
|
||
| `Esc` или `h` | Прервать скрипт и закрыть popup |
|
||
|
||
> Vim-motions (`j`/`k`/`l`/`h`) отключаются автоматически во время текстового ввода — символы идут в поле ввода как обычно.
|
||
|
||
---
|
||
|
||
## Конфигурация
|
||
|
||
При первом запуске клиент запрашивает URL сервера прямо в терминале — до входа в TUI:
|
||
|
||
```
|
||
╔══════════════════════════════════╗
|
||
║ Ostiary — первый запуск ║
|
||
╚══════════════════════════════════╝
|
||
|
||
Конфиг будет сохранён в:
|
||
/home/user/.config/ostiary/config.toml
|
||
|
||
URL сервера меню: https://your-server/api/menu
|
||
```
|
||
|
||
После ввода создаётся `~/.config/ostiary/config.toml`. Его можно редактировать вручную:
|
||
|
||
```toml
|
||
server_url = "https://your-server/api/menu"
|
||
timeout_sec = 10
|
||
update_api = "https://git.example.com/api/v1/repos/user/ostiary"
|
||
|
||
[theme]
|
||
selected_bg = "blue"
|
||
```
|
||
|
||
| Параметр | Описание |
|
||
|---|---|
|
||
| `server_url` | URL эндпоинта `GET /api/menu` |
|
||
| `timeout_sec` | Таймаут HTTP-запросов в секундах |
|
||
| `update_api` | Базовый URL Gitea API для проверки обновлений (опционально) |
|
||
|
||
---
|
||
|
||
## Автообновление
|
||
|
||
Если задан `update_api`, при каждом запуске клиент в фоне проверяет наличие нового релиза в Gitea. Если найдена новая стабильная версия — показывает диалог:
|
||
|
||
```
|
||
Доступно обновление
|
||
1.0.0 → 1.1.0
|
||
Размер: 4.2 МБ
|
||
|
||
[Y / l] Обновить [N / h / Esc] Пропустить
|
||
```
|
||
|
||
При подтверждении скачивает бинарник, атомарно заменяет текущий исполняемый файл и перезапускает приложение через `execv` (процесс не пересоздаётся, PID остаётся тем же).
|
||
|
||
### Naming convention ассетов в Gitea
|
||
|
||
Файлы релиза должны называться по шаблону `{name}-{os}-{arch}`:
|
||
|
||
| Файл | Платформа |
|
||
|---|---|
|
||
| `ostiary-linux-x86_64` | Linux 64-bit |
|
||
| `ostiary-linux-aarch64` | Linux ARM64 |
|
||
| `ostiary-macos-aarch64` | macOS Apple Silicon |
|
||
| `ostiary-macos-x86_64` | macOS Intel |
|
||
|
||
---
|
||
|
||
## Формат меню (JSON)
|
||
|
||
Сервер отдаёт `GET /api/menu` с `Content-Type: application/json`.
|
||
|
||
### Корневая структура
|
||
|
||
```json
|
||
{
|
||
"version": "1.0",
|
||
"menu": [ ...пункты... ]
|
||
}
|
||
```
|
||
|
||
### Категория
|
||
|
||
Содержит вложенные пункты. Отображается с иконкой `📁`.
|
||
|
||
```json
|
||
{
|
||
"id": "bitrix",
|
||
"title": "1С Битрикс",
|
||
"description": "Инструменты администрирования",
|
||
"children": [
|
||
...пункты меню...
|
||
]
|
||
}
|
||
```
|
||
|
||
### Действие
|
||
|
||
Отображается с иконкой `▶`.
|
||
|
||
```json
|
||
{
|
||
"id": "db_manage",
|
||
"title": "Управление БД",
|
||
"description": "Дамп, импорт, подключение к базе",
|
||
"action": {
|
||
"type": "bash",
|
||
"script": "#!/bin/bash\necho hello",
|
||
"interaction": "structured",
|
||
"confirm": false
|
||
}
|
||
}
|
||
```
|
||
|
||
#### `confirm` и `confirm_message`
|
||
|
||
Если `confirm: true`, перед запуском появится диалог подтверждения.
|
||
`confirm_message` переопределяет текст вопроса.
|
||
|
||
```json
|
||
{
|
||
"id": "drop_db",
|
||
"title": "Удалить базу данных",
|
||
"action": {
|
||
"type": "bash",
|
||
"script": "...",
|
||
"interaction": "structured",
|
||
"confirm": true,
|
||
"confirm_message": "База данных будет удалена без возможности восстановления. Продолжить?"
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Типы действий
|
||
|
||
### `bash` — запуск скрипта
|
||
|
||
```json
|
||
{
|
||
"type": "bash",
|
||
"script": "#!/bin/bash\n...",
|
||
"interaction": "structured",
|
||
"confirm": false
|
||
}
|
||
```
|
||
|
||
| Параметр | Значения | Описание |
|
||
|---|---|---|
|
||
| `script` | строка | Полный текст bash-скрипта включая shebang |
|
||
| `interaction` | `"structured"` / `"terminal"` | Режим взаимодействия |
|
||
| `confirm` | `true` / `false` | Запросить подтверждение перед запуском |
|
||
| `confirm_message` | строка | Кастомный текст диалога подтверждения |
|
||
|
||
**`interaction: "terminal"`** — скрипт запускается дочерним процессом без захвата вывода.
|
||
|
||
**`interaction: "structured"`** — весь вывод отображается в прокручиваемом логе с ANSI-цветами, скрипт может запрашивать ввод через протокол команд.
|
||
|
||
### `download` — скачивание файла
|
||
|
||
```json
|
||
{
|
||
"type": "download",
|
||
"url": "https://example.com/file.tar.gz",
|
||
"filename": "backup.tar.gz",
|
||
"target_dir": "/var/backups",
|
||
"confirm": false
|
||
}
|
||
```
|
||
|
||
### `download_and_run` — скачать и выполнить
|
||
|
||
```json
|
||
{
|
||
"type": "download_and_run",
|
||
"url": "https://example.com/installer.sh",
|
||
"run_args": ["--silent", "--prefix=/opt"],
|
||
"keep_file": false,
|
||
"confirm": true
|
||
}
|
||
```
|
||
|
||
### `http_request` — HTTP-запрос
|
||
|
||
```json
|
||
{
|
||
"type": "http_request",
|
||
"method": "POST",
|
||
"url": "https://api.example.com/deploy",
|
||
"headers": { "Authorization": "Bearer token123" },
|
||
"body": "{\"env\": \"production\"}",
|
||
"confirm": true
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Интерактивные скрипты (structured)
|
||
|
||
При `interaction: "structured"` скрипт общается с клиентом через протокол команд. Команды отправляются в **stderr** (`>&2`), ответы приходят в **stdin**.
|
||
|
||
### Базовые хелперы
|
||
|
||
```bash
|
||
#!/bin/bash
|
||
|
||
# Отправить команду и получить ответ от пользователя
|
||
ask() {
|
||
printf 'CMD:%s\n' "$1" >&2
|
||
read -r _response
|
||
printf '%s' "$_response"
|
||
}
|
||
|
||
# Отправить уведомление (не ждёт ответа, не блокирует скрипт)
|
||
notify() {
|
||
printf 'CMD:%s\n' "$1" >&2
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Ввод текста (`input`)
|
||
|
||
```json
|
||
{"type":"input","prompt":"Введите имя пользователя:","default":"admin","secret":false}
|
||
```
|
||
|
||
| Поле | Описание |
|
||
|---|---|
|
||
| `prompt` | Заголовок поля ввода |
|
||
| `default` | Значение по умолчанию (опционально) |
|
||
| `secret` | Если `true` — символы заменяются на `•` |
|
||
|
||
```bash
|
||
username=$(ask '{"type":"input","prompt":"Имя пользователя:","default":"admin"}')
|
||
password=$(ask '{"type":"input","prompt":"Пароль:","secret":true}')
|
||
```
|
||
|
||
---
|
||
|
||
### Выбор из списка (`menu`)
|
||
|
||
Список с навигацией `↑`/`↓`/`j`/`k`, выбор по `Enter`/`l`. Возвращает `id` выбранного пункта.
|
||
|
||
```json
|
||
{
|
||
"type": "menu",
|
||
"prompt": "Выберите действие:",
|
||
"options": [
|
||
{"id": "dump", "label": "Создать дамп базы"},
|
||
{"id": "import", "label": "Импортировать дамп"},
|
||
{"id": "connect","label": "Подключиться к БД"}
|
||
]
|
||
}
|
||
```
|
||
|
||
```bash
|
||
action=$(ask '{"type":"menu","prompt":"Выберите действие:","options":[
|
||
{"id":"dump","label":"Создать дамп"},
|
||
{"id":"import","label":"Импортировать"},
|
||
{"id":"exit","label":"Отмена"}
|
||
]}')
|
||
|
||
case $action in
|
||
dump) echo "Создаю дамп..." ;;
|
||
import) echo "Импортирую..." ;;
|
||
exit) exit 0 ;;
|
||
esac
|
||
```
|
||
|
||
**Динамический список** (из массива bash):
|
||
```bash
|
||
files=($(ls *.sql 2>/dev/null))
|
||
|
||
options='['
|
||
for i in "${!files[@]}"; do
|
||
[ $i -gt 0 ] && options+=','
|
||
escaped=$(printf '%s' "${files[$i]}" | sed 's/"/\\"/g')
|
||
options+="{\"id\":\"$i\",\"label\":\"${escaped}\"}"
|
||
done
|
||
options+=']'
|
||
|
||
idx=$(ask "{\"type\":\"menu\",\"prompt\":\"Выберите файл:\",\"options\":${options}}")
|
||
selected="${files[$idx]}"
|
||
```
|
||
|
||
---
|
||
|
||
### Подтверждение (`confirm`)
|
||
|
||
Показывает вопрос с кнопками `[Y]`/`[N]`. Клавиши `Y`/`N`/`l`/`h` обрабатываются без Enter.
|
||
|
||
```json
|
||
{"type":"confirm","prompt":"Данные будут перезаписаны. Продолжить?"}
|
||
```
|
||
|
||
Ответ: `"y"` или `"n"`.
|
||
|
||
```bash
|
||
answer=$(ask '{"type":"confirm","prompt":"Удалить временные файлы?"}')
|
||
if [[ "$answer" =~ ^[Yy]$ ]]; then
|
||
rm -rf /tmp/myapp_*
|
||
fi
|
||
```
|
||
|
||
---
|
||
|
||
### Сообщение (`message`)
|
||
|
||
Показывает сообщение с цветом. Скрипт продолжается после нажатия `Enter`.
|
||
|
||
```json
|
||
{"type":"message","level":"info","text":"Операция завершена успешно!"}
|
||
```
|
||
|
||
| `level` | Цвет |
|
||
|---|---|
|
||
| `"info"` | Зелёный |
|
||
| `"warn"` | Жёлтый |
|
||
| `"error"` | Красный |
|
||
|
||
```bash
|
||
if ! command -v mysqldump &> /dev/null; then
|
||
ask '{"type":"message","level":"error","text":"mysqldump не найден."}' > /dev/null
|
||
exit 1
|
||
fi
|
||
```
|
||
|
||
> `> /dev/null` — сбрасывает пустой ответ клиента чтобы он не попал в переменную.
|
||
|
||
---
|
||
|
||
### Прогресс (`progress`)
|
||
|
||
Обновляет прогресс-бар не блокируя скрипт (используйте `notify`, не `ask`).
|
||
|
||
```json
|
||
{"type":"progress","percent":45,"message":"Копирование файлов..."}
|
||
```
|
||
|
||
```bash
|
||
notify '{"type":"progress","percent":0,"message":"Начало..."}'
|
||
mysqldump -h "$host" -u "$user" "$db" > backup.sql
|
||
notify '{"type":"progress","percent":70,"message":"Сжимаю..."}'
|
||
gzip backup.sql
|
||
notify '{"type":"progress","percent":100,"message":"Готово!"}'
|
||
```
|
||
|
||
---
|
||
|
||
### Запуск внешнего приложения (`exec`)
|
||
|
||
Временно **передаёт терминал** внешней программе (mysql, vim, htop и др.). Клиент скрывает TUI, программа работает на полном экране, после завершения TUI восстанавливается. Возвращает код завершения.
|
||
|
||
```json
|
||
{"type":"exec","shell":"mysql -h localhost -u root mydb"}
|
||
```
|
||
|
||
```bash
|
||
# Интерактивная сессия mysql
|
||
exit_code=$(ask "{\"type\":\"exec\",\"shell\":\"mysql -h \\\"$host\\\" -u \\\"$user\\\" -p\\\"$pass\\\" \\\"$db\\\"\"}")
|
||
echo "Сессия завершена (код: $exit_code)"
|
||
|
||
# Редактирование конфига
|
||
ask '{"type":"exec","shell":"vim /etc/nginx/nginx.conf"}' > /dev/null
|
||
systemctl reload nginx
|
||
|
||
# Мониторинг ресурсов
|
||
ask '{"type":"exec","shell":"htop"}' > /dev/null
|
||
```
|
||
|
||
---
|
||
|
||
## Полный пример скрипта
|
||
|
||
```bash
|
||
#!/bin/bash
|
||
|
||
ask() { printf 'CMD:%s\n' "$1" >&2; read -r _r; printf '%s' "$_r"; }
|
||
notify() { printf 'CMD:%s\n' "$1" >&2; }
|
||
|
||
host=$(ask '{"type":"input","prompt":"Хост:","default":"localhost"}')
|
||
db=$(ask '{"type":"input","prompt":"База данных:"}')
|
||
user=$(ask '{"type":"input","prompt":"Пользователь:","default":"root"}')
|
||
pass=$(ask '{"type":"input","prompt":"Пароль:","secret":true}')
|
||
|
||
action=$(ask "{\"type\":\"menu\",\"prompt\":\"Выберите действие:\",\"options\":[
|
||
{\"id\":\"dump\", \"label\":\"Создать дамп\"},
|
||
{\"id\":\"shell\", \"label\":\"Открыть mysql-консоль\"},
|
||
{\"id\":\"cancel\",\"label\":\"Отмена\"}
|
||
]}")
|
||
|
||
case $action in
|
||
dump)
|
||
confirm=$(ask '{"type":"confirm","prompt":"Создать дамп базы данных?"}')
|
||
[[ ! "$confirm" =~ ^[Yy]$ ]] && exit 0
|
||
|
||
backup="backup_${db}_$(date +%Y%m%d_%H%M%S).sql"
|
||
notify '{"type":"progress","percent":10,"message":"Создание дампа..."}'
|
||
|
||
if mysqldump -h "$host" -u "$user" -p"$pass" "$db" > "$backup"; then
|
||
notify '{"type":"progress","percent":100,"message":"Готово!"}'
|
||
echo "Дамп: $backup ($(du -h "$backup" | cut -f1))"
|
||
else
|
||
ask '{"type":"message","level":"error","text":"Ошибка при создании дампа!"}' > /dev/null
|
||
exit 1
|
||
fi
|
||
;;
|
||
|
||
shell)
|
||
code=$(ask "{\"type\":\"exec\",\"shell\":\"mysql -h \\\"$host\\\" -u \\\"$user\\\" -p\\\"$pass\\\" \\\"$db\\\"\"}")
|
||
echo "Сессия завершена с кодом $code"
|
||
;;
|
||
|
||
cancel) exit 0 ;;
|
||
esac
|
||
```
|
||
|
||
---
|
||
|
||
## Структура сервера
|
||
|
||
```
|
||
server/
|
||
├── index.php # Собирает JSON: menu_structure.json + scripts/
|
||
├── menu_structure.json # Иерархия меню (script_file вместо script)
|
||
└── scripts/
|
||
├── bitrix_db_manage.sh
|
||
├── bitrix_monitoring.sh
|
||
├── network_suspicious_connections.sh
|
||
└── wordpress_db_backup.sh
|
||
```
|
||
|
||
В `menu_structure.json` вместо поля `"script"` используется `"script_file"`:
|
||
|
||
```json
|
||
{
|
||
"id": "db_manage",
|
||
"title": "Управление БД",
|
||
"action": {
|
||
"type": "bash",
|
||
"script_file": "bitrix_db_manage.sh",
|
||
"interaction": "structured",
|
||
"confirm": false
|
||
}
|
||
}
|
||
```
|
||
|
||
`index.php` читает файл скрипта при каждом запросе — скрипты редактируются без изменения JSON.
|
||
|
||
### Добавление нового пункта меню
|
||
|
||
1. Создать `server/scripts/category_action.sh`
|
||
2. Добавить пункт в `menu_structure.json`:
|
||
|
||
```json
|
||
{
|
||
"id": "my_action",
|
||
"title": "Название в меню",
|
||
"description": "Описание для нижней панели",
|
||
"action": {
|
||
"type": "bash",
|
||
"script_file": "category_action.sh",
|
||
"interaction": "structured",
|
||
"confirm": false
|
||
}
|
||
}
|
||
```
|
||
|
||
Соглашение по именованию файлов: `{категория}_{подкатегория}_{действие}.sh`
|
||
|
||
---
|
||
|
||
## Сборка релиза
|
||
|
||
Скрипт `release.sh` собирает бинарники для всех платформ сразу и кладёт их в `dist/`:
|
||
|
||
```bash
|
||
cd Ostiary
|
||
./release.sh
|
||
# → dist/ostiary-linux-x86_64
|
||
# → dist/ostiary-linux-aarch64
|
||
# → dist/ostiary-macos-aarch64
|
||
```
|
||
|
||
После сборки загрузите файлы из `dist/` в [новый релиз Gitea](https://git.vainend.com/admin/ostiary/releases/new). Имена файлов должны точно совпадать с тем, что скачивает `install.sh`.
|
||
|
||
**Вручную — Linux:**
|
||
```bash
|
||
cargo build --release
|
||
# → target/release/ostiary
|
||
```
|
||
|
||
**Вручную — кросс-компиляция macOS → Linux x86_64 (musl, статический):**
|
||
```bash
|
||
rustup target add x86_64-unknown-linux-musl
|
||
brew install FiloSottile/musl-cross/musl-cross
|
||
|
||
CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=x86_64-linux-musl-gcc \
|
||
cargo build --release --target x86_64-unknown-linux-musl
|
||
# → target/x86_64-unknown-linux-musl/release/ostiary
|
||
```
|
||
|
||
**Вручную — ARM64 Linux:**
|
||
```bash
|
||
rustup target add aarch64-unknown-linux-musl
|
||
|
||
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=aarch64-linux-musl-gcc \
|
||
cargo build --release --target aarch64-unknown-linux-musl
|
||
# → target/aarch64-unknown-linux-musl/release/ostiary
|
||
```
|
||
|
||
Бинарник собирается статически (musl) и не требует никаких зависимостей на целевом сервере.
|