feat(tui): add safe client and backup workflows
This commit is contained in:
+294
-26
@@ -54,7 +54,7 @@ print_usage() {
|
||||
Использование:
|
||||
wg-manager.sh help
|
||||
wg-manager.sh version
|
||||
wg-manager.sh {status|list|add NAME [--mode MODE] [--endpoint HOST]|show NAME|config NAME [--show]|qr NAME|remove NAME [--yes]|diagnose [--quiet]|backup|backups|restore BACKUP [--yes]|log [LIMIT]|tui}
|
||||
wg-manager.sh {status|list|add NAME [--mode MODE] [--split-networks CIDRS] [--endpoint HOST]|show NAME|config NAME [--show]|qr NAME|remove NAME [--yes]|diagnose [--quiet]|backup|backups|restore BACKUP [--yes]|log [LIMIT]|tui}
|
||||
|
||||
Команды управления WireGuard будут добавлены в следующих версиях.
|
||||
USAGE
|
||||
@@ -836,7 +836,7 @@ transaction_subshell_exit() {
|
||||
local status=$?
|
||||
transaction_cleanup
|
||||
if [[ -n ${TRANSACTION_RESULT_FILE:-} ]]; then
|
||||
printf 'STATUS=%s\nBACKUP=%s\nOUTCOME=%s\n' "$status" "${TRANSACTION_BACKUP_PATH:-}" "${TRANSACTION_OUTCOME:-failed-before-apply}" >"$TRANSACTION_RESULT_FILE"
|
||||
printf 'STATUS=%s\nBACKUP=%s\nFINALIZER=%s\nOUTCOME=%s\n' "$status" "${TRANSACTION_BACKUP_PATH:-}" "${REMOVE_ARCHIVE_PATH:-}" "${TRANSACTION_OUTCOME:-failed-before-apply}" >"$TRANSACTION_RESULT_FILE"
|
||||
if ! chmod 600 -- "$TRANSACTION_RESULT_FILE"; then
|
||||
printf 'Предупреждение: не удалось защитить transaction result\n' >&2
|
||||
fi
|
||||
@@ -986,7 +986,7 @@ run_transaction() {
|
||||
die "$EXIT_USAGE" 'Ожидаются operation, target и builder транзакции'
|
||||
return $?
|
||||
fi
|
||||
local operation=$1 target=$2 builder=$3 result_file result_status result_backup result_outcome had_errexit=0
|
||||
local operation=$1 target=$2 builder=$3 result_file result_status result_backup result_finalizer result_outcome had_errexit=0
|
||||
shift 3
|
||||
[[ -n ${WG_MANAGER_CONFIG:-} ]] || { die "$EXIT_VALIDATION" 'Не задан manager config'; return $?; }
|
||||
set_runtime_paths || return $?
|
||||
@@ -1005,11 +1005,12 @@ run_transaction() {
|
||||
(( had_errexit == 1 )) && set -e || set +e
|
||||
if [[ -r $result_file ]]; then
|
||||
while IFS='=' read -r key value; do
|
||||
case $key in STATUS) result_status=$value ;; BACKUP) result_backup=$value ;; OUTCOME) result_outcome=$value ;; esac
|
||||
case $key in STATUS) result_status=$value ;; BACKUP) result_backup=$value ;; FINALIZER) result_finalizer=$value ;; OUTCOME) result_outcome=$value ;; esac
|
||||
done <"$result_file"
|
||||
fi
|
||||
rm -f -- "$result_file"
|
||||
TRANSACTION_BACKUP_PATH=${result_backup:-}
|
||||
[[ -n ${result_finalizer:-} ]] && REMOVE_ARCHIVE_RESULT=$result_finalizer
|
||||
TRANSACTION_OUTCOME=${result_outcome:-failed-before-apply}
|
||||
if [[ $result_outcome == success ]]; then
|
||||
if ! write_audit_event apply "$target" success success >/dev/null 2>&1; then warn 'Не удалось записать аудит применения'; fi
|
||||
@@ -1097,7 +1098,7 @@ allocate_client_ipv6() {
|
||||
client_allowed_ips() {
|
||||
case $1 in
|
||||
full-tunnel) printf '0.0.0.0/0,::/0' ;;
|
||||
split-tunnel) printf '%s.0/24' "$VPN_SUBNET_PREFIX" ;;
|
||||
split-tunnel) printf '%s' "${ADD_SPLIT_NETWORKS:-${VPN_SUBNET_PREFIX}.0/24}" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
@@ -1294,12 +1295,12 @@ clear_add_transaction_state() {
|
||||
for path in "$ADD_META_STAGE" "$ADD_CONFIG_STAGE" "$ADD_KEY_STAGE" "$ADD_PUB_STAGE" "$ADD_PSK_STAGE"; do
|
||||
[[ -n ${path:-} ]] && rm -f -- "$path"
|
||||
done
|
||||
unset ADD_NAME ADD_MODE ADD_ENDPOINT ADD_PRIVATE_KEY ADD_PUBLIC_KEY ADD_PSK ADD_META_STAGE ADD_CONFIG_STAGE ADD_KEY_STAGE ADD_PUB_STAGE ADD_PSK_STAGE ADD_PUBLISH_READY_FIFO ADD_PUBLISH_RELEASE_FIFO ADD_PUBLISH_PID_FILE ADD_PUBLISH_READY_INDEX
|
||||
unset ADD_NAME ADD_MODE ADD_ENDPOINT ADD_SPLIT_NETWORKS ADD_PRIVATE_KEY ADD_PUBLIC_KEY ADD_PSK ADD_META_STAGE ADD_CONFIG_STAGE ADD_KEY_STAGE ADD_PUB_STAGE ADD_PSK_STAGE ADD_PUBLISH_READY_FIFO ADD_PUBLISH_RELEASE_FIFO ADD_PUBLISH_PID_FILE ADD_PUBLISH_READY_INDEX
|
||||
TRANSACTION_FINALIZER=''
|
||||
}
|
||||
|
||||
add_client_impl() {
|
||||
local name mode endpoint arg mode_seen=0 endpoint_seen=0
|
||||
local name mode endpoint arg split_networks='' mode_seen=0 endpoint_seen=0 split_seen=0
|
||||
(($# >= 1)) || { die "$EXIT_USAGE" 'Ожидается имя клиента'; return $?; }
|
||||
name=$1; shift
|
||||
load_config || return $?
|
||||
@@ -1314,11 +1315,22 @@ add_client_impl() {
|
||||
(( endpoint_seen == 0 )) || { die "$EXIT_USAGE" 'Флаг --endpoint указан повторно'; return $?; }
|
||||
(($# >= 2)) || { die "$EXIT_USAGE" 'Для --endpoint требуется значение'; return $?; }
|
||||
endpoint=$2; endpoint_seen=1; shift 2 ;;
|
||||
--split-networks)
|
||||
(( split_seen == 0 )) || { die "$EXIT_USAGE" 'Флаг --split-networks указан повторно'; return $?; }
|
||||
(($# >= 2)) || { die "$EXIT_USAGE" 'Для --split-networks требуется значение'; return $?; }
|
||||
split_networks=$2; split_seen=1; shift 2 ;;
|
||||
*) die "$EXIT_USAGE" 'Неизвестный или лишний аргумент add'; return $? ;;
|
||||
esac
|
||||
done
|
||||
validate_client_name "$name" || { die "$EXIT_VALIDATION" 'Некорректное имя клиента'; return $?; }
|
||||
[[ $mode == full-tunnel || $mode == split-tunnel ]] || { die "$EXIT_VALIDATION" 'Некорректный режим клиента'; return $?; }
|
||||
if [[ $mode == split-tunnel ]]; then
|
||||
[[ -n $split_networks ]] || split_networks="${VPN_SUBNET_PREFIX}.0/24"
|
||||
split_networks=$(normalize_allowed_ips "$split_networks") || { die "$EXIT_VALIDATION" 'Некорректные сети split-tunnel'; return $?; }
|
||||
elif [[ -n $split_networks ]]; then
|
||||
die "$EXIT_VALIDATION" '--split-networks доступен только для split-tunnel'
|
||||
return $?
|
||||
fi
|
||||
validate_endpoint "$endpoint" || { die "$EXIT_VALIDATION" 'Некорректный endpoint клиента'; return $?; }
|
||||
require_add_privilege || return $?
|
||||
ADD_META_STAGE='' ADD_CONFIG_STAGE='' ADD_KEY_STAGE='' ADD_PUB_STAGE='' ADD_PSK_STAGE=''
|
||||
@@ -1367,7 +1379,7 @@ add_client_impl() {
|
||||
clear_add_transaction_state
|
||||
return "$EXIT_OPERATION"
|
||||
fi
|
||||
ADD_NAME=$name ADD_MODE=$mode ADD_ENDPOINT=$endpoint
|
||||
ADD_NAME=$name ADD_MODE=$mode ADD_ENDPOINT=$endpoint ADD_SPLIT_NETWORKS=$split_networks
|
||||
TRANSACTION_FINALIZER=publish_add_artifacts
|
||||
local status
|
||||
if run_transaction add "$name" add_client_builder; then
|
||||
@@ -1376,7 +1388,7 @@ add_client_impl() {
|
||||
status=$?
|
||||
fi
|
||||
if (( status != 0 )); then
|
||||
if ! AUDIT_CLIENT=$name AUDIT_KEY_SUFFIX='' write_audit_event add "$name" failed transaction_failed >/dev/null 2>&1; then warn 'Не удалось записать аудит добавления'; fi
|
||||
if ! AUDIT_CLIENT=$name AUDIT_KEY_SUFFIX='' write_audit_event add "$name" failed transaction-failed >/dev/null 2>&1; then warn 'Не удалось записать аудит добавления'; fi
|
||||
clear_add_transaction_state
|
||||
return "$status"
|
||||
fi
|
||||
@@ -1448,6 +1460,8 @@ gum_header() { gum_command style --border double --padding '0 1' "$1"; }
|
||||
gum_choose() { gum_command choose "$@"; }
|
||||
gum_filter() { gum_command filter --placeholder 'Выберите клиента' "$@"; }
|
||||
gum_input() { gum_command input "$@"; }
|
||||
gum_input_placeholder() { gum_command input --placeholder "$1"; }
|
||||
gum_input_prompt() { gum_command input --prompt "$1"; }
|
||||
gum_confirm() { gum_command confirm "$@"; }
|
||||
gum_table() { gum_command table "$@"; }
|
||||
gum_pager() { gum_command pager "$@"; }
|
||||
@@ -1496,7 +1510,202 @@ tui_select_client() {
|
||||
fi
|
||||
done
|
||||
((index >= 0)) || return "$EXIT_VALIDATION"
|
||||
tui_page_command print_client_details "${CLIENT_NAMES[$index]}" "$index"
|
||||
tui_client_action_flow "${CLIENT_NAMES[$index]}" "$index"
|
||||
}
|
||||
|
||||
tui_client_action_flow() {
|
||||
local name=$1 index=$2 action config_output status
|
||||
local -a actions=('Детали' 'Показать конфигурацию' 'Показать QR' 'Удалить' 'Назад')
|
||||
if [[ ${CLIENT_ORIGINS[$index]} != managed ]]; then actions=('Детали' 'Назад'); fi
|
||||
action=$(gum_choose "${actions[@]}") || return 0
|
||||
case $action in
|
||||
'Детали') tui_page_command print_client_details "$name" "$index" ;;
|
||||
'Показать конфигурацию')
|
||||
gum_confirm 'Показать полную конфигурацию?' || return 0
|
||||
if config_output=$(export_client_config_exact "$name" "${CLIENT_KEYS[$index]}" 1); then
|
||||
printf '%s' "$config_output" | gum_pager
|
||||
else status=$?; return "$status"; fi
|
||||
;;
|
||||
'Показать QR')
|
||||
gum_confirm 'Показать QR-код?' || return 0
|
||||
if qr_client_exact "$name" "${CLIENT_KEYS[$index]}"; then
|
||||
:
|
||||
else
|
||||
status=$?
|
||||
if (( status == EXIT_DEPENDENCY )); then
|
||||
printf 'QR-код недоступен; путь конфигурации показан выше\n' >&2
|
||||
return 0
|
||||
fi
|
||||
printf 'QR-код недоступен или не создан\n' >&2
|
||||
return "$status"
|
||||
fi
|
||||
;;
|
||||
'Удалить') tui_remove_flow_for_index "$name" "$index" ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
tui_add_flow() {
|
||||
local name mode status networks endpoint preview config_path endpoint_prompt endpoint_attempts=0
|
||||
local -a add_args=()
|
||||
local attempts=0
|
||||
while ((attempts < 2)); do
|
||||
name=$(gum_input_placeholder 'Имя клиента') || return 0
|
||||
if validate_client_name "$name"; then break; fi
|
||||
printf 'Некорректное имя клиента, повторите ввод\n' >&2
|
||||
((attempts += 1)); ((attempts < 2)) || { die "$EXIT_VALIDATION" 'Некорректное имя клиента'; return $?; }
|
||||
done
|
||||
mode=$(gum_choose 'Полный туннель' 'Выборочные сети') || return 0
|
||||
case $mode in
|
||||
'Полный туннель') mode=full-tunnel ;;
|
||||
'Выборочные сети') mode=split-tunnel ;;
|
||||
*) return "$EXIT_VALIDATION" ;;
|
||||
esac
|
||||
networks=''
|
||||
if [[ $mode == split-tunnel ]]; then
|
||||
networks=$(gum_input_placeholder 'Сети через запятую') || return 0
|
||||
networks=$(normalize_allowed_ips "$networks") || { die "$EXIT_VALIDATION" 'Некорректные сети split-tunnel'; return $?; }
|
||||
fi
|
||||
endpoint=$SERVER_ENDPOINT
|
||||
endpoint_prompt=$(tui_wrap_value 'Использовать endpoint:' "$endpoint")
|
||||
if ! gum_confirm "$endpoint_prompt"; then
|
||||
while ((endpoint_attempts < 2)); do
|
||||
endpoint=$(gum_input_placeholder 'Endpoint HOST') || return 0
|
||||
if validate_endpoint "$endpoint"; then break; fi
|
||||
printf 'Некорректный endpoint, повторите ввод\n' >&2
|
||||
((endpoint_attempts += 1))
|
||||
done
|
||||
validate_endpoint "$endpoint" || { die "$EXIT_VALIDATION" 'Некорректный endpoint'; return $?; }
|
||||
fi
|
||||
printf -v preview 'Имя: %s\nРежим: %s\nСети: %s\nEndpoint: %s' "$name" "$mode" "${networks:--}" "$endpoint"
|
||||
gum_pager <<<"$preview" || return $?
|
||||
gum_confirm 'Создать клиента с этими параметрами?' || return 0
|
||||
add_args=("$name" --mode "$mode" --endpoint "$endpoint")
|
||||
[[ -n $networks ]] && add_args+=(--split-networks "$networks")
|
||||
if add_client "${add_args[@]}"; then
|
||||
config_path=$(export_client_config "$name" 0) || return $?
|
||||
printf 'Клиент добавлен: %s\nКонфигурация: %s\n' "$name" "$config_path"
|
||||
if gum_confirm 'Показать QR-код?'; then
|
||||
if qr_client "$name"; then
|
||||
:
|
||||
else
|
||||
status=$?
|
||||
if (( status == EXIT_DEPENDENCY )); then
|
||||
printf 'QR-код недоступен; путь конфигурации показан выше\n' >&2
|
||||
return 0
|
||||
fi
|
||||
printf 'QR-код недоступен или не создан\n' >&2
|
||||
return "$status"
|
||||
fi
|
||||
fi
|
||||
else status=$?; printf 'Добавление клиента не выполнено\n' >&2; return "$status"; fi
|
||||
}
|
||||
|
||||
tui_remove_flow() {
|
||||
local index selected_index=-1 name key label base previous ordinal duplicate
|
||||
load_client_model || return $?
|
||||
local -a choices=() choice_indices=()
|
||||
for index in "${!CLIENT_KEYS[@]}"; do
|
||||
[[ ${CLIENT_ORIGINS[$index]} == managed ]] || continue
|
||||
base="${CLIENT_NAMES[$index]:0:32} — ${CLIENT_ADDRESSES[$index]:0:24} — ${CLIENT_KEYS[$index]:0:8}"
|
||||
ordinal=1; duplicate=0
|
||||
for previous in "${!CLIENT_KEYS[@]}"; do
|
||||
[[ $previous -ge $index ]] && break
|
||||
[[ ${CLIENT_ORIGINS[$previous]} == managed ]] || continue
|
||||
if [[ "${CLIENT_NAMES[$previous]:0:32} — ${CLIENT_ADDRESSES[$previous]:0:24} — ${CLIENT_KEYS[$previous]:0:8}" == "$base" ]]; then ((ordinal += 1)); duplicate=1; fi
|
||||
done
|
||||
for previous in "${!CLIENT_KEYS[@]}"; do
|
||||
((previous > index)) || continue
|
||||
[[ ${CLIENT_ORIGINS[$previous]} == managed ]] || continue
|
||||
if [[ "${CLIENT_NAMES[$previous]:0:32} — ${CLIENT_ADDRESSES[$previous]:0:24} — ${CLIENT_KEYS[$previous]:0:8}" == "$base" ]]; then duplicate=1; break; fi
|
||||
done
|
||||
if ((duplicate == 1)); then label="${CLIENT_NAMES[$index]:0:28} — ${CLIENT_ADDRESSES[$index]:0:24} — ${CLIENT_KEYS[$index]:0:8} #${ordinal}"; else label=$base; fi
|
||||
choices+=("$label")
|
||||
choice_indices+=("$index")
|
||||
done
|
||||
(("${#choices[@]} > 0")) || { printf 'Управляемые клиенты отсутствуют\n'; return 0; }
|
||||
key=$(gum_filter "${choices[@]}") || return 0
|
||||
for previous in "${!choices[@]}"; do
|
||||
if [[ ${choices[$previous]} == "$key" ]]; then
|
||||
((selected_index < 0)) || return "$EXIT_VALIDATION"
|
||||
selected_index=${choice_indices[$previous]}
|
||||
fi
|
||||
done
|
||||
((selected_index >= 0)) || return "$EXIT_VALIDATION"
|
||||
name=${CLIENT_NAMES[$selected_index]}; key=${CLIENT_KEYS[$selected_index]}
|
||||
local confirmation address_lines
|
||||
address_lines=$(tui_wrap_value 'Адреса:' "${CLIENT_ADDRESSES[$selected_index]}")
|
||||
confirmation=$(printf 'Удалить клиента:\nИмя: %s\n%s\nКлюч: *%s\nАрхивировать его файлы?' "$name" "$address_lines" "$(short_public_key "$key")")
|
||||
gum_confirm "$confirmation" || return 0
|
||||
if remove_client_exact "$name" "$key"; then
|
||||
printf 'Клиент удалён и архивирован: %s\n' "$name"
|
||||
[[ -n ${REMOVE_ARCHIVE_RESULT:-} ]] && printf 'Архив: %s\n' "$REMOVE_ARCHIVE_RESULT"
|
||||
unset REMOVE_ARCHIVE_RESULT
|
||||
else
|
||||
return $?
|
||||
fi
|
||||
}
|
||||
|
||||
tui_remove_flow_for_index() {
|
||||
local name=$1 index=$2 key=${CLIENT_KEYS[$2]} status
|
||||
local confirmation address_lines
|
||||
address_lines=$(tui_wrap_value 'Адреса:' "${CLIENT_ADDRESSES[$index]}")
|
||||
confirmation=$(printf 'Удалить клиента:\nИмя: %s\n%s\nКлюч: *%s\nАрхивировать его файлы?' "$name" "$address_lines" "$(short_public_key "$key")")
|
||||
gum_confirm "$confirmation" || return 0
|
||||
if remove_client_exact "$name" "$key"; then
|
||||
printf 'Клиент удалён и архивирован: %s\n' "$name"
|
||||
[[ -n ${REMOVE_ARCHIVE_RESULT:-} ]] && printf 'Архив: %s\n' "$REMOVE_ARCHIVE_RESULT"
|
||||
unset REMOVE_ARCHIVE_RESULT
|
||||
return 0
|
||||
else
|
||||
status=$?
|
||||
unset REMOVE_ARCHIVE_RESULT
|
||||
return "$status"
|
||||
fi
|
||||
}
|
||||
|
||||
tui_backup_flow() {
|
||||
local action selected listing backup_id details valid_selection
|
||||
local -a backups=()
|
||||
action=$(gum_choose 'Создать резервную копию' 'Восстановить резервную копию' 'Отмена') || return 0
|
||||
case $action in
|
||||
'Создать резервную копию')
|
||||
local backup_path
|
||||
if backup_path=$(manual_backup); then
|
||||
printf 'Резервная копия создана: %s\n' "$backup_path"
|
||||
else return $?; fi
|
||||
;;
|
||||
'Восстановить резервную копию')
|
||||
if listing=$(list_backups); then :; else return $?; fi
|
||||
local -a backup_details=()
|
||||
while IFS=$'\t' read -r backup_id details; do
|
||||
[[ $backup_id =~ ^[0-9]{8}T[0-9]{6}Z-[A-Za-z0-9._-]+-[0-9]{3}$ ]] || continue
|
||||
backups+=("$backup_id")
|
||||
backup_details+=("$details")
|
||||
done <<<"$listing"
|
||||
((${#backups[@]} > 0)) || { printf 'Подходящие резервные копии отсутствуют\n'; return 0; }
|
||||
selected=$(gum_choose "${backups[@]}") || return 0
|
||||
valid_selection=0
|
||||
for backup_id in "${backups[@]}"; do [[ $backup_id == "$selected" ]] && valid_selection=1; done
|
||||
((valid_selection == 1)) || { die "$EXIT_VALIDATION" 'Выбрана неизвестная резервная копия'; return $?; }
|
||||
local selected_detail=''
|
||||
for index in "${!backups[@]}"; do
|
||||
[[ ${backups[$index]} == "$selected" ]] && selected_detail=${backup_details[$index]}
|
||||
done
|
||||
printf 'Резервная копия: %s\n%s\n' "$selected" "$selected_detail" | gum_pager || return $?
|
||||
local typed_confirmation
|
||||
typed_confirmation=$(gum_input_prompt "Введите идентификатор $selected") || return 0
|
||||
[[ $typed_confirmation == "$selected" ]] || return 0
|
||||
restore_backup "$selected" --yes ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
tui_run_action() {
|
||||
local status
|
||||
if "$@"; then return 0; else status=$?; fi
|
||||
printf 'Предупреждение: действие TUI не выполнено (код %s)\n' "$status" >&2
|
||||
return "$status"
|
||||
}
|
||||
|
||||
tui_screen() {
|
||||
@@ -1518,12 +1727,13 @@ tui_screen() {
|
||||
if gum_header "$dashboard"; then :; else status=$?; unset TUI_GUM_PATH; return "$status"; fi
|
||||
if choice=$(gum_choose "${menu_labels[@]}"); then :; else unset TUI_GUM_PATH; return 0; fi
|
||||
case $choice in
|
||||
Состояние) tui_page_command print_status || ((failures += 1)) ;;
|
||||
Клиенты) tui_select_client || ((failures += 1)) ;;
|
||||
Диагностика) tui_page_command diagnose || ((failures += 1)) ;;
|
||||
Журнал) tui_page_command list_audit || ((failures += 1)) ;;
|
||||
'Резервные копии') tui_page_command list_backups || ((failures += 1)) ;;
|
||||
Добавить|Удалить) printf 'Действие «%s» доступно через CLI в M9; изменения не выполняются.\n' "$choice" ;;
|
||||
Состояние) if tui_run_action tui_page_command print_status; then :; else status=$?; ((status == EXIT_ROLLBACK)) && { unset TUI_GUM_PATH; return "$status"; }; ((failures += 1)); fi ;;
|
||||
Клиенты) if tui_run_action tui_select_client; then :; else status=$?; ((status == EXIT_ROLLBACK)) && { unset TUI_GUM_PATH; return "$status"; }; ((failures += 1)); fi ;;
|
||||
Диагностика) if tui_run_action tui_page_command diagnose; then :; else status=$?; ((status == EXIT_ROLLBACK)) && { unset TUI_GUM_PATH; return "$status"; }; ((failures += 1)); fi ;;
|
||||
Журнал) if tui_run_action tui_page_command list_audit; then :; else status=$?; ((status == EXIT_ROLLBACK)) && { unset TUI_GUM_PATH; return "$status"; }; ((failures += 1)); fi ;;
|
||||
'Резервные копии') if tui_run_action tui_backup_flow; then :; else status=$?; ((status == EXIT_ROLLBACK)) && { unset TUI_GUM_PATH; return "$status"; }; ((failures += 1)); fi ;;
|
||||
Добавить) if tui_run_action tui_add_flow; then :; else status=$?; ((status == EXIT_ROLLBACK)) && { unset TUI_GUM_PATH; return "$status"; }; ((failures += 1)); fi ;;
|
||||
Удалить) if tui_run_action tui_remove_flow; then :; else status=$?; ((status == EXIT_ROLLBACK)) && { unset TUI_GUM_PATH; return "$status"; }; ((failures += 1)); fi ;;
|
||||
Выход|'') unset TUI_GUM_PATH; return 0 ;;
|
||||
*) printf 'Неизвестный пункт TUI\n' >&2; ((failures += 1)) ;;
|
||||
esac
|
||||
@@ -1768,7 +1978,7 @@ restore_backup() {
|
||||
if (( status == 0 )); then
|
||||
write_audit_event restore "$name" success success >/dev/null 2>&1 || { cleanup_restore_snapshot; die "$EXIT_OPERATION" 'Восстановление выполнено, но аудит не записан'; return "$EXIT_OPERATION"; }
|
||||
else
|
||||
if ! write_audit_event restore "$name" failed transaction_failed >/dev/null 2>&1; then warn 'Не удалось записать аудит восстановления'; fi
|
||||
if ! write_audit_event restore "$name" failed transaction-failed >/dev/null 2>&1; then warn 'Не удалось записать аудит восстановления'; fi
|
||||
fi
|
||||
cleanup_restore_snapshot
|
||||
return "$status"
|
||||
@@ -2005,13 +2215,14 @@ print_client_details() {
|
||||
if [[ ${CLIENT_ORIGINS[$index]} == managed ]]; then printf 'Режим: %s\nСоздан: %s\nПуть конфигурации: %s\n' "${META_MODE[$key]}" "${META_CREATED[$key]}" "$config_path"; fi
|
||||
}
|
||||
|
||||
export_client_config() {
|
||||
local name=$1 show=${2:-0} index key path status=0
|
||||
export_client_config_core() {
|
||||
local name=$1 show=${2:-0} index key path status=0 expected_key=${3:-}
|
||||
[[ $show == 0 || $show == 1 ]] || { die "$EXIT_USAGE" 'Недопустимый режим экспорта конфигурации'; return $?; }
|
||||
load_client_model || return $?
|
||||
index=$(resolve_named_client "$name") || return $?
|
||||
[[ ${CLIENT_ORIGINS[$index]} == managed ]] || { die "$EXIT_VALIDATION" 'Для этого клиента нет безопасной managed-конфигурации'; return $?; }
|
||||
key=${CLIENT_KEYS[$index]}
|
||||
[[ -z $expected_key || $expected_key == "$key" ]] || { die "$EXIT_VALIDATION" 'Идентичность клиента изменилась'; return $?; }
|
||||
path=$(validate_client_config_path "$key" "$name" "$index") || return $?
|
||||
if (( show == 0 )); then printf '%s\n' "$path"; return 0; fi
|
||||
if open_validated_client_config "$path"; then :; else
|
||||
@@ -2031,6 +2242,16 @@ export_client_config() {
|
||||
if (( status != 0 )); then die "$EXIT_OPERATION" 'Не удалось прочитать конфигурацию клиента'; return $?; fi
|
||||
}
|
||||
|
||||
export_client_config() {
|
||||
(($# >= 1 && $# <= 2)) || { die "$EXIT_USAGE" 'Лишние аргументы config'; return $?; }
|
||||
export_client_config_core "$1" "${2:-0}"
|
||||
}
|
||||
|
||||
export_client_config_exact() {
|
||||
local name=$1 expected_key=$2 show=${3:-0}
|
||||
export_client_config_core "$name" "$show" "$expected_key"
|
||||
}
|
||||
|
||||
show_client() {
|
||||
local name=$1 index
|
||||
load_client_model || return $?
|
||||
@@ -2038,12 +2259,13 @@ show_client() {
|
||||
print_client_details "$name" "$index"
|
||||
}
|
||||
|
||||
qr_client() {
|
||||
local name=$1 index key path qr_path status
|
||||
qr_client_core() {
|
||||
local name=$1 expected_key=${2:-} index key path qr_path status
|
||||
load_client_model || return $?
|
||||
index=$(resolve_named_client "$name") || return $?
|
||||
[[ ${CLIENT_ORIGINS[$index]} == managed ]] || { die "$EXIT_VALIDATION" 'Для этого клиента нет безопасной managed-конфигурации'; return $?; }
|
||||
key=${CLIENT_KEYS[$index]}
|
||||
[[ -z $expected_key || $expected_key == "$key" ]] || { die "$EXIT_VALIDATION" 'Идентичность клиента изменилась'; return $?; }
|
||||
path=$(validate_client_config_path "$key" "$name" "$index") || return $?
|
||||
if qr_path=$(command -v qrencode 2>/dev/null); then :; else qr_path=''; fi
|
||||
if [[ -z $qr_path || ! -f $qr_path || -L $qr_path || ! -x $qr_path ]]; then
|
||||
@@ -2068,8 +2290,27 @@ qr_client() {
|
||||
(( status == 0 )) || { die "$EXIT_OPERATION" 'Не удалось сформировать QR-код'; return "$EXIT_OPERATION"; }
|
||||
}
|
||||
|
||||
qr_client() {
|
||||
(($# == 1)) || { die "$EXIT_USAGE" 'Лишние аргументы qr'; return $?; }
|
||||
qr_client_core "$1"
|
||||
}
|
||||
|
||||
qr_client_exact() {
|
||||
local name=$1 expected_key=$2
|
||||
qr_client_core "$name" "$expected_key"
|
||||
}
|
||||
|
||||
short_public_key() { local key=$1; printf '%s' "${key: -8}"; }
|
||||
|
||||
tui_wrap_value() {
|
||||
local label=$1 value=$2 width=72
|
||||
printf '%s\n' "$label"
|
||||
while [[ -n $value ]]; do
|
||||
printf '%s\n' "${value:0:width}"
|
||||
value=${value:width}
|
||||
done
|
||||
}
|
||||
|
||||
remove_peer_builder() {
|
||||
local stage=$1 target_key=${REMOVE_KEY:?} target_name=${REMOVE_NAME:?}
|
||||
local -a lines=() kept=() block=()
|
||||
@@ -2312,17 +2553,20 @@ confirm_remove() {
|
||||
|
||||
remove_is_interactive() { [[ -t 0 && -t 1 ]]; }
|
||||
|
||||
remove_client_impl() {
|
||||
local name=$1 flag=${2:-} index key path status answer
|
||||
(($# <= 2)) || { die "$EXIT_USAGE" 'Лишние аргументы remove'; return $?; }
|
||||
remove_client_core() {
|
||||
local name=$1 expected_key=$2 flag=$3 index key path status answer
|
||||
REMOVE_ARCHIVE_RESULT=''
|
||||
validate_client_name "$name" || { die "$EXIT_VALIDATION" 'Некорректное имя клиента'; return $?; }
|
||||
[[ -z $flag || $flag == --yes ]] || { die "$EXIT_USAGE" 'Разрешён только флаг remove --yes'; return $?; }
|
||||
load_client_model || return $?
|
||||
path_components_safe "$ARCHIVE_DIR" || { die "$EXIT_VALIDATION" 'Каталог архива содержит символическую ссылку'; return $?; }
|
||||
require_remove_privilege || return $?
|
||||
index=$(resolve_named_client "$name") || return $?
|
||||
[[ ${CLIENT_ORIGINS[$index]} == managed ]] || { die "$EXIT_VALIDATION" 'Удалять можно только managed-клиента'; return $?; }
|
||||
key=${CLIENT_KEYS[$index]}; path=$(validate_client_config_path "$key" "$name" "$index") || return $?
|
||||
if [[ -n $expected_key && $expected_key != "$key" ]]; then
|
||||
die "$EXIT_VALIDATION" 'Идентичность клиента изменилась до удаления'
|
||||
return $?
|
||||
fi
|
||||
REMOVE_NAME=$name REMOVE_KEY=$key REMOVE_CONFIG_PATH=$path REMOVE_META_FILE=${META_FILE[$key]}
|
||||
REMOVE_ALLOWED=${DISK_ALLOWED[$key]}
|
||||
if [[ -z $flag ]] && ! remove_is_interactive; then
|
||||
@@ -2341,15 +2585,30 @@ remove_client_impl() {
|
||||
TRANSACTION_FINALIZER=archive_remove_artifacts
|
||||
if run_transaction remove "$name" remove_peer_builder "$REMOVE_META_FILE"; then status=0; else status=$?; fi
|
||||
if (( status == 0 )); then
|
||||
[[ -n ${REMOVE_ARCHIVE_PATH:-} ]] && REMOVE_ARCHIVE_RESULT=$REMOVE_ARCHIVE_PATH
|
||||
REMOVE_ERROR_CATEGORY=success
|
||||
if ! remove_audit success; then printf 'Предупреждение: удаление выполнено, но не удалось записать аудит (частичный результат)\n' >&2; status=$EXIT_OPERATION; fi
|
||||
elif { REMOVE_ERROR_CATEGORY=transaction_failed; ! remove_audit failed; }; then
|
||||
elif { REMOVE_ERROR_CATEGORY=transaction-failed; ! remove_audit failed; }; then
|
||||
printf 'Предупреждение: операция удаления завершилась ошибкой, аудит не записан\n' >&2
|
||||
fi
|
||||
unset REMOVE_NAME REMOVE_KEY REMOVE_CONFIG_PATH REMOVE_META_FILE REMOVE_ALLOWED REMOVE_ARCHIVE_PATH TRANSACTION_FINALIZER
|
||||
return "$status"
|
||||
}
|
||||
|
||||
remove_client_impl() {
|
||||
local name='' flag='' arg
|
||||
if (($# > 0)); then name=$1; shift; fi
|
||||
while (($#)); do
|
||||
arg=$1
|
||||
case $arg in
|
||||
--yes) [[ -z $flag ]] || { die "$EXIT_USAGE" 'Флаг remove --yes указан повторно'; return $?; }; flag=--yes ;;
|
||||
*) die "$EXIT_USAGE" 'Лишние аргументы remove'; return $? ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
remove_client_core "$name" '' "$flag"
|
||||
}
|
||||
|
||||
remove_client() {
|
||||
local xtrace=0 status
|
||||
[[ $- == *x* ]] && xtrace=1; set +x
|
||||
@@ -2359,6 +2618,15 @@ remove_client() {
|
||||
return "$status"
|
||||
}
|
||||
|
||||
remove_client_exact() {
|
||||
local name=$1 expected_key=$2 status xtrace=0
|
||||
[[ $- == *x* ]] && xtrace=1
|
||||
set +x
|
||||
if remove_client_core "$name" "$expected_key" --yes; then status=0; else status=$?; fi
|
||||
(( xtrace == 1 )) && set -x
|
||||
return "$status"
|
||||
}
|
||||
|
||||
main() {
|
||||
local status
|
||||
if (( $# == 0 )); then
|
||||
|
||||
Reference in New Issue
Block a user