feat(clients): add safe client removal and archival
This commit is contained in:
+300
-7
@@ -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|diagnose|logs|backups|restore|tui}
|
||||
wg-manager.sh {status|list|add NAME [--mode MODE] [--endpoint HOST]|show NAME|config NAME [--show]|qr NAME|remove NAME [--yes]|diagnose|logs|backups|restore|tui}
|
||||
|
||||
Команды управления WireGuard будут добавлены в следующих версиях.
|
||||
USAGE
|
||||
@@ -583,7 +583,7 @@ transaction_test_hooks_enabled() {
|
||||
config_path=${CONFIG_PATH:-${WG_MANAGER_CONFIG:-}}
|
||||
[[ -n $config_path && $config_path == "${TEST_ROOT%/}"/* ]] || return 1
|
||||
path_components_safe "$config_path" || return 1
|
||||
for path in "${WG_CONF:-}" "${CLIENT_DIR:-}" "${METADATA_DIR:-}" "${BACKUP_DIR:-}" "${LOCK_FILE:-}"; do
|
||||
for path in "${WG_CONF:-}" "${CLIENT_DIR:-}" "${METADATA_DIR:-}" "${BACKUP_DIR:-}" "${LOCK_FILE:-}" "${LOG_DIR:-}" "${ARCHIVE_DIR:-}"; do
|
||||
[[ -n $path && $path == "${TEST_ROOT%/}"/* ]] && path_components_safe "$path" || return 1
|
||||
done
|
||||
for path in "${ADD_PUBLISH_READY_FIFO:-}" "${ADD_PUBLISH_RELEASE_FIFO:-}" "${ADD_PUBLISH_PID_FILE:-}"; do
|
||||
@@ -841,7 +841,7 @@ transaction_subshell_exit() {
|
||||
|
||||
run_transaction_core() {
|
||||
local operation=$1 target=$2 builder=$3 metadata lock_parent lock_fd status=0 rollback_status stage new_stripped old_stripped original_copy prior_dump current_dump
|
||||
local source_mode source_uid source_gid backup_path
|
||||
local source_mode source_uid source_gid backup_path finalizer_status=0
|
||||
local transaction_path
|
||||
shift 3
|
||||
TRANSACTION_TEMP_PATHS=(); TRANSACTION_BACKUP_PATH=''
|
||||
@@ -928,10 +928,12 @@ run_transaction_core() {
|
||||
TRANSACTION_OWNED_PATHS=()
|
||||
fi
|
||||
else
|
||||
status=$EXIT_OPERATION
|
||||
finalizer_status=$?
|
||||
printf 'Ошибка: не удалось завершить финальную операцию транзакции\n' >&2
|
||||
status=$finalizer_status
|
||||
fi
|
||||
fi
|
||||
if [[ -n ${TRANSACTION_PENDING_SIGNAL:-} ]]; then
|
||||
if [[ -n ${TRANSACTION_PENDING_SIGNAL:-} && $status != $EXIT_ROLLBACK ]]; then
|
||||
printf 'Ошибка: операция прервана сигналом %s\n' "$TRANSACTION_PENDING_SIGNAL" >&2
|
||||
status=$EXIT_OPERATION
|
||||
fi
|
||||
@@ -952,7 +954,7 @@ run_transaction_core() {
|
||||
printf 'КРИТИЧНО: откат прерван сигналом %s\n' "$TRANSACTION_ROLLBACK_SIGNAL" >&2
|
||||
status=$EXIT_ROLLBACK
|
||||
else
|
||||
status=$EXIT_OPERATION
|
||||
if (( finalizer_status == EXIT_ROLLBACK )); then status=$EXIT_ROLLBACK; else status=$EXIT_OPERATION; fi
|
||||
fi
|
||||
fi
|
||||
TRANSACTION_ROLLBACK_ACTIVE=0
|
||||
@@ -1012,6 +1014,14 @@ require_add_privilege() {
|
||||
}
|
||||
}
|
||||
|
||||
require_remove_privilege() {
|
||||
if (( EUID == 0 )); then return 0; fi
|
||||
transaction_test_hooks_enabled || {
|
||||
die "$EXIT_PERMISSION" 'Для удаления клиента требуются права root'
|
||||
return $?
|
||||
}
|
||||
}
|
||||
|
||||
client_paths_available() {
|
||||
local name=$1 path
|
||||
for path in "${METADATA_DIR}/${name}.meta" "${CLIENT_DIR}/${name}.conf" "${CLIENT_DIR}/${name}.key" "${CLIENT_DIR}/${name}.pub" "${CLIENT_DIR}/${name}.psk"; do
|
||||
@@ -1523,6 +1533,285 @@ qr_client() {
|
||||
(( status == 0 )) || { die "$EXIT_OPERATION" 'Не удалось сформировать QR-код'; return "$EXIT_OPERATION"; }
|
||||
}
|
||||
|
||||
short_public_key() { local key=$1; printf '%s' "${key: -8}"; }
|
||||
|
||||
remove_peer_builder() {
|
||||
local stage=$1 target_key=${REMOVE_KEY:?} target_name=${REMOVE_NAME:?}
|
||||
local -a lines=() kept=() block=()
|
||||
local line index peer_start block_start block_end cursor=0 key marker allowed normalized matched=0 metadata_allowed
|
||||
[[ ${META_NAME[$target_key]:-} == "$target_name" && ${META_MANAGED[$target_key]:-0} == 1 ]] || { die "$EXIT_VALIDATION" 'Identity клиента изменилась после lock'; return $?; }
|
||||
[[ ${META_FILE[$target_key]:-} == "${REMOVE_META_FILE:-}" && ${META_CONFIG[$target_key]:-} == "${REMOVE_CONFIG_PATH:-}" ]] || { die "$EXIT_VALIDATION" 'Путь клиента изменился после lock'; return $?; }
|
||||
normalized=$(normalize_allowed_ips "${DISK_ALLOWED[$target_key]:-}") || { die "$EXIT_VALIDATION" 'Адреса клиента изменились после lock'; return $?; }
|
||||
[[ $normalized == "${REMOVE_ALLOWED:-}" ]] || { die "$EXIT_VALIDATION" 'Адреса клиента изменились после lock'; return $?; }
|
||||
metadata_allowed=${META_IPV4[$target_key]:-}
|
||||
[[ -n $metadata_allowed ]] && metadata_allowed+='/32'
|
||||
[[ -n ${META_IPV6[$target_key]:-} ]] && { [[ -n $metadata_allowed ]] && metadata_allowed+=','; metadata_allowed+=${META_IPV6[$target_key]}/128; }
|
||||
normalized=$(normalize_allowed_ips "$metadata_allowed") || { die "$EXIT_VALIDATION" 'Адреса metadata изменились после lock'; return $?; }
|
||||
[[ $normalized == "${REMOVE_ALLOWED:-}" ]] || { die "$EXIT_VALIDATION" 'Адреса metadata изменились после lock'; return $?; }
|
||||
mapfile -t lines <"$WG_CONF" || return "$EXIT_OPERATION"
|
||||
while :; do
|
||||
peer_start=-1
|
||||
for index in "${!lines[@]}"; do
|
||||
(( index >= cursor )) || continue
|
||||
if [[ ${lines[$index]} =~ ^[[:space:]]*\[Peer\][[:space:]]*$ ]]; then peer_start=$index; break; fi
|
||||
done
|
||||
if (( peer_start < 0 )); then
|
||||
kept+=("${lines[@]:$cursor}")
|
||||
break
|
||||
fi
|
||||
block_start=$peer_start
|
||||
if (( peer_start > 0 )) && [[ ${lines[$((peer_start - 1))]} == "# wg-manager-client: ${target_name}" ]]; then
|
||||
block_start=$((peer_start - 1))
|
||||
if (( block_start < cursor )) && ((${#kept[@]} > 0)) && [[ ${kept[$((${#kept[@]} - 1))]} == "# wg-manager-client: ${target_name}" ]]; then
|
||||
unset 'kept[${#kept[@]}-1]'
|
||||
fi
|
||||
fi
|
||||
if (( block_start > cursor )); then kept+=("${lines[@]:$cursor:$((block_start - cursor))}"); fi
|
||||
block_end=${#lines[@]}
|
||||
for index in "${!lines[@]}"; do
|
||||
(( index > peer_start )) || continue
|
||||
if [[ ${lines[$index]} =~ ^[[:space:]]*\[Peer\][[:space:]]*$ || ${lines[$index]} =~ ^[[:space:]]*\[Interface\][[:space:]]*$ ]]; then block_end=$index; break; fi
|
||||
done
|
||||
block=("${lines[@]:$block_start:$((block_end - block_start))}")
|
||||
key=''; marker=''; allowed=''
|
||||
for line in "${block[@]}"; do
|
||||
[[ $line =~ ^[[:space:]]*PublicKey[[:space:]]*=[[:space:]]*(.*)$ ]] && key=$(trim_space "${BASH_REMATCH[1]}")
|
||||
[[ $line =~ ^[[:space:]]*AllowedIPs[[:space:]]*=[[:space:]]*(.*)$ ]] && allowed=$(trim_space "${BASH_REMATCH[1]}")
|
||||
if [[ ${block[0]:-} == "# wg-manager-client: ${target_name}" ]]; then marker=1; fi
|
||||
done
|
||||
normalized=$(normalize_allowed_ips "$allowed") || return "$EXIT_VALIDATION"
|
||||
if [[ $key == "$target_key" && $marker == 1 && $normalized == "${REMOVE_ALLOWED:-}" ]]; then
|
||||
# A conventional marker immediately before the next [Peer] belongs to
|
||||
# that next peer and must survive removal of this block.
|
||||
local trailing_index=$(( ${#block[@]} - 1 ))
|
||||
while (( trailing_index >= 0 )) && [[ -z ${block[$trailing_index]} ]]; do ((trailing_index -= 1)); done
|
||||
if (( trailing_index >= 0 )) && [[ ${block[$trailing_index]} =~ ^#\ (wg-manager-client:|###\ Client\ ) ]] && [[ ${block[$trailing_index]} != "# wg-manager-client: ${target_name}" ]]; then
|
||||
kept+=("${block[$trailing_index]}")
|
||||
fi
|
||||
else
|
||||
kept+=("${block[@]}")
|
||||
fi
|
||||
if [[ $key == "$target_key" && $marker == 1 && $normalized == "${REMOVE_ALLOWED:-}" ]]; then ((matched += 1)); fi
|
||||
cursor=$block_end
|
||||
done
|
||||
(( matched == 1 )) || { die "$EXIT_VALIDATION" 'Не найден единственный точный managed-блок клиента'; return $?; }
|
||||
: >"$stage" || return "$EXIT_OPERATION"
|
||||
printf '%s\n' "${kept[@]}" >"$stage"
|
||||
}
|
||||
|
||||
remove_archive_sources() {
|
||||
local path base mode owner server_owner
|
||||
REMOVE_ARCHIVE_SOURCES=("$REMOVE_META_FILE" "$REMOVE_CONFIG_PATH")
|
||||
for base in key pub psk; do
|
||||
path="${CLIENT_DIR}/${REMOVE_NAME}.${base}"
|
||||
if [[ -e $path || -L $path ]]; then REMOVE_ARCHIVE_SOURCES+=("$path"); fi
|
||||
done
|
||||
server_owner=$(stat -c '%u:%g' -- "$WG_CONF") || { die "$EXIT_OPERATION" 'Не удалось проверить владельца серверной конфигурации'; return $?; }
|
||||
for path in "${REMOVE_ARCHIVE_SOURCES[@]}"; do
|
||||
[[ -f $path && ! -L $path ]] || { die "$EXIT_VALIDATION" 'Небезопасный файл архивации'; return $?; }
|
||||
mode=$(stat -c '%a' -- "$path") || { die "$EXIT_OPERATION" 'Не удалось проверить режим файла архивации'; return $?; }
|
||||
owner=$(stat -c '%u:%g' -- "$path") || { die "$EXIT_OPERATION" 'Не удалось проверить владельца файла архивации'; return $?; }
|
||||
[[ $mode == 600 && $owner == "$server_owner" && $(stat -c '%h' -- "$path") == 1 ]] || { die "$EXIT_VALIDATION" 'Файл архивации имеет небезопасные атрибуты'; return $?; }
|
||||
done
|
||||
}
|
||||
|
||||
allocate_remove_archive() {
|
||||
local now stamp base candidate suffix
|
||||
now=$(current_epoch) || { die "$EXIT_OPERATION" 'Не удалось получить время архивации'; return $?; }
|
||||
stamp=$(date -u -d "@${now}" +%Y%m%dT%H%M%SZ) || { die "$EXIT_OPERATION" 'Не удалось получить UTC-время архивации'; return $?; }
|
||||
base="${ARCHIVE_DIR}/${stamp}-${WG_INTERFACE}-${REMOVE_NAME}"
|
||||
for ((suffix = 0; suffix <= 999; suffix++)); do
|
||||
if (( suffix == 0 )); then candidate=$base; else candidate="${base}-${suffix}"; fi
|
||||
if mkdir -- "$candidate" 2>/dev/null; then
|
||||
if ! chmod 700 -- "$candidate"; then
|
||||
rm -rf -- "$candidate"
|
||||
die "$EXIT_OPERATION" 'Не удалось защитить каталог архива'
|
||||
return $?
|
||||
fi
|
||||
REMOVE_ARCHIVE_PATH=$candidate
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
die "$EXIT_OPERATION" 'Не удалось создать уникальный архив'
|
||||
return $?
|
||||
}
|
||||
|
||||
archive_remove_artifacts() {
|
||||
local path base mode owner server_owner remove_index=0 copy_index=0
|
||||
local -a archived=()
|
||||
set_runtime_paths || return $?
|
||||
[[ ${REMOVE_NAME:?} =~ ^[A-Za-z0-9._-]+$ ]] || return "$EXIT_VALIDATION"
|
||||
path_components_safe "$ARCHIVE_DIR" && path_components_safe "$CLIENT_DIR" && path_components_safe "$METADATA_DIR" || { die "$EXIT_VALIDATION" 'Каталоги архивации содержат небезопасный путь'; return $?; }
|
||||
[[ ! -L $ARCHIVE_DIR ]] || { die "$EXIT_VALIDATION" 'Каталог архива является ссылкой'; return $?; }
|
||||
[[ $REMOVE_META_FILE == "${METADATA_DIR}/${REMOVE_NAME}.meta" && $REMOVE_CONFIG_PATH == "${CLIENT_DIR}/${REMOVE_NAME}.conf" ]] || { die "$EXIT_VALIDATION" 'Файлы клиента не соответствуют его имени'; return $?; }
|
||||
server_owner=$(stat -c '%u:%g' -- "$WG_CONF") || { die "$EXIT_OPERATION" 'Не удалось проверить владельца серверной конфигурации'; return $?; }
|
||||
if [[ -e $ARCHIVE_DIR ]]; then
|
||||
[[ -d $ARCHIVE_DIR ]] || { die "$EXIT_VALIDATION" 'Каталог архива недоступен'; return $?; }
|
||||
mode=$(stat -c '%a' -- "$ARCHIVE_DIR") || { die "$EXIT_OPERATION" 'Не удалось проверить режим каталога архива'; return $?; }
|
||||
owner=$(stat -c '%u:%g' -- "$ARCHIVE_DIR") || { die "$EXIT_OPERATION" 'Не удалось проверить владельца каталога архива'; return $?; }
|
||||
[[ $mode == 700 && $owner == "$server_owner" ]] || { die "$EXIT_VALIDATION" 'Каталог архива имеет небезопасные атрибуты'; return $?; }
|
||||
else
|
||||
mkdir -- "$ARCHIVE_DIR" || { die "$EXIT_OPERATION" 'Не удалось создать каталог архива'; return $?; }
|
||||
if ! chmod 700 -- "$ARCHIVE_DIR"; then
|
||||
if ! rmdir -- "$ARCHIVE_DIR" 2>/dev/null; then printf 'Предупреждение: не удалось удалить незавершённый каталог архива\n' >&2; fi
|
||||
die "$EXIT_OPERATION" 'Не удалось защитить каталог архива'
|
||||
return $?
|
||||
fi
|
||||
if ! owner=$(stat -c '%u:%g' -- "$ARCHIVE_DIR"); then
|
||||
if ! rmdir -- "$ARCHIVE_DIR" 2>/dev/null; then printf 'Предупреждение: не удалось удалить незавершённый каталог архива\n' >&2; fi
|
||||
die "$EXIT_OPERATION" 'Не удалось проверить владельца каталога архива'
|
||||
return $?
|
||||
fi
|
||||
if [[ $owner != "$server_owner" ]]; then
|
||||
if ! rmdir -- "$ARCHIVE_DIR" 2>/dev/null; then printf 'Предупреждение: не удалось удалить незавершённый каталог архива\n' >&2; fi
|
||||
die "$EXIT_VALIDATION" 'Владелец каталога архива не совпадает'
|
||||
return $?
|
||||
fi
|
||||
fi
|
||||
remove_archive_sources || return $?
|
||||
allocate_remove_archive || return $?
|
||||
for path in "${REMOVE_ARCHIVE_SOURCES[@]}"; do
|
||||
base=${path##*/}
|
||||
if [[ ${M6_ARCHIVE_COPY_FAIL_INDEX:-} == "$copy_index" ]] && transaction_test_hooks_enabled; then
|
||||
rm -rf -- "$REMOVE_ARCHIVE_PATH"
|
||||
die "$EXIT_OPERATION" 'Не удалось сохранить файл клиента в архив'
|
||||
return $?
|
||||
fi
|
||||
if ! copy_restricted "$path" "${REMOVE_ARCHIVE_PATH}/${base}"; then
|
||||
rm -rf -- "$REMOVE_ARCHIVE_PATH"
|
||||
die "$EXIT_OPERATION" 'Не удалось сохранить файл клиента в архив'
|
||||
return $?
|
||||
fi
|
||||
archived+=("$path")
|
||||
((copy_index += 1))
|
||||
done
|
||||
for path in "${REMOVE_ARCHIVE_SOURCES[@]}"; do
|
||||
if [[ ${M6_ARCHIVE_REMOVE_FAIL_INDEX:-} == "$remove_index" ]] && transaction_test_hooks_enabled; then
|
||||
for base in "${archived[@]}"; do
|
||||
[[ -e $base ]] && continue
|
||||
if [[ ${M6_ARCHIVE_RECOVERY_FAIL_INDEX:-} == "$remove_index" ]] && transaction_test_hooks_enabled; then
|
||||
die "$EXIT_ROLLBACK" 'КРИТИЧНО: не удалось восстановить файл клиента'
|
||||
return $?
|
||||
fi
|
||||
cp -- "${REMOVE_ARCHIVE_PATH}/${base##*/}" "$base" || { die "$EXIT_ROLLBACK" 'КРИТИЧНО: не удалось восстановить файл клиента'; return $?; }
|
||||
chmod 600 -- "$base" || { die "$EXIT_ROLLBACK" 'КРИТИЧНО: не удалось защитить восстановленный файл'; return $?; }
|
||||
done
|
||||
die "$EXIT_OPERATION" 'Не удалось удалить исходный файл клиента после архивации'
|
||||
return $?
|
||||
fi
|
||||
if ! rm -f -- "$path"; then
|
||||
for base in "${archived[@]}"; do
|
||||
[[ -e $base ]] && continue
|
||||
if [[ ${M6_ARCHIVE_RECOVERY_FAIL_INDEX:-} == "$remove_index" ]] && transaction_test_hooks_enabled; then
|
||||
die "$EXIT_ROLLBACK" 'КРИТИЧНО: не удалось восстановить файл клиента'
|
||||
return $?
|
||||
fi
|
||||
cp -- "${REMOVE_ARCHIVE_PATH}/${base##*/}" "$base" || { die "$EXIT_ROLLBACK" 'КРИТИЧНО: не удалось восстановить файл клиента'; return $?; }
|
||||
chmod 600 -- "$base" || { die "$EXIT_ROLLBACK" 'КРИТИЧНО: не удалось защитить восстановленный файл'; return $?; }
|
||||
done
|
||||
die "$EXIT_OPERATION" 'Не удалось завершить архивацию файлов клиента'
|
||||
return $?
|
||||
fi
|
||||
((remove_index += 1))
|
||||
done
|
||||
}
|
||||
|
||||
remove_audit() {
|
||||
local outcome=$1 audit_file=${LOG_DIR}/wg-manager.log mode owner server_owner timestamp fd expected_identity opened_identity
|
||||
path_components_safe "$LOG_DIR" || return "$EXIT_VALIDATION"
|
||||
[[ ! -L $LOG_DIR ]] || return "$EXIT_VALIDATION"
|
||||
if [[ ! -e $LOG_DIR ]]; then
|
||||
mkdir -- "$LOG_DIR" || return "$EXIT_OPERATION"
|
||||
chmod 700 -- "$LOG_DIR" || return "$EXIT_OPERATION"
|
||||
fi
|
||||
[[ -d $LOG_DIR ]] || return "$EXIT_OPERATION"
|
||||
mode=$(stat -c '%a' -- "$LOG_DIR") || return "$EXIT_OPERATION"
|
||||
[[ $mode == 700 ]] || return "$EXIT_VALIDATION"
|
||||
server_owner=$(stat -c '%u:%g' -- "$WG_CONF") || return "$EXIT_OPERATION"
|
||||
owner=$(stat -c '%u:%g' -- "$LOG_DIR") || return "$EXIT_OPERATION"
|
||||
[[ $owner == "$server_owner" ]] || return "$EXIT_VALIDATION"
|
||||
if [[ ${M6_AUDIT_FAIL:-0} == 1 ]] && transaction_test_hooks_enabled; then
|
||||
return "$EXIT_OPERATION"
|
||||
fi
|
||||
if [[ -e $audit_file || -L $audit_file ]]; then
|
||||
[[ -f $audit_file && ! -L $audit_file && $(stat -c '%h' -- "$audit_file") == 1 ]] || return "$EXIT_VALIDATION"
|
||||
mode=$(stat -c '%a' -- "$audit_file") || return "$EXIT_OPERATION"
|
||||
[[ $mode == 600 ]] || return "$EXIT_VALIDATION"
|
||||
owner=$(stat -c '%u:%g' -- "$audit_file") || return "$EXIT_OPERATION"
|
||||
[[ $owner == "$server_owner" ]] || return "$EXIT_VALIDATION"
|
||||
else
|
||||
: >"$audit_file" || return "$EXIT_OPERATION"
|
||||
chmod 600 -- "$audit_file" || return "$EXIT_OPERATION"
|
||||
fi
|
||||
timestamp=$(current_epoch) || return "$EXIT_OPERATION"
|
||||
timestamp=$(date -u -d "@${timestamp}" +%Y-%m-%dT%H:%M:%SZ) || return "$EXIT_OPERATION"
|
||||
expected_identity=$(stat -c '%d:%i:%a:%u:%g:%h' -- "$audit_file") || return "$EXIT_OPERATION"
|
||||
exec {fd}>>"$audit_file" || return "$EXIT_OPERATION"
|
||||
opened_identity=$(stat -Lc '%d:%i:%a:%u:%g:%h' -- "/proc/self/fd/${fd}") || { exec {fd}>&-; return "$EXIT_OPERATION"; }
|
||||
[[ $opened_identity == "$expected_identity" ]] || { exec {fd}>&-; return "$EXIT_VALIDATION"; }
|
||||
if ! printf 'operation=remove client=%s target=%s key_suffix=%s timestamp=%s outcome=%s actor_uid=%s error=%s\n' "$REMOVE_NAME" "$REMOVE_NAME" "$(short_public_key "$REMOVE_KEY")" "$timestamp" "$outcome" "$EUID" "${REMOVE_ERROR_CATEGORY:-$outcome}" >&$fd; then
|
||||
exec {fd}>&-
|
||||
return "$EXIT_OPERATION"
|
||||
fi
|
||||
exec {fd}>&-
|
||||
}
|
||||
|
||||
confirm_remove() {
|
||||
local name=$1 key=$2 addresses=$3 answer
|
||||
printf 'Удалить клиента %s (ключ *%s, адреса %s)? [y/N] ' "$name" "$(short_public_key "$key")" "$addresses" >&2
|
||||
IFS= read -r answer || answer=n
|
||||
[[ $answer == y || $answer == Y ]]
|
||||
}
|
||||
|
||||
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 $?; }
|
||||
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 $?
|
||||
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
|
||||
REMOVE_ERROR_CATEGORY=confirmation_required
|
||||
remove_audit cancelled
|
||||
die "$EXIT_USAGE" 'В неинтерактивном режиме требуется флаг --yes'
|
||||
return $?
|
||||
fi
|
||||
if [[ -n $flag ]]; then answer=y; else if confirm_remove "$name" "$key" "${CLIENT_ADDRESSES[$index]}"; then answer=y; else answer=n; fi; fi
|
||||
if [[ $answer != y && $answer != Y ]]; then
|
||||
REMOVE_ERROR_CATEGORY=cancelled
|
||||
if ! remove_audit cancelled; then printf 'Предупреждение: отмена выполнена, но аудит не записан\n' >&2; fi
|
||||
printf 'Операция отменена\n' >&2
|
||||
return 0
|
||||
fi
|
||||
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
|
||||
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
|
||||
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() {
|
||||
local xtrace=0 status
|
||||
[[ $- == *x* ]] && xtrace=1; set +x
|
||||
if remove_client_impl "$@"; then status=0; else status=$?; fi
|
||||
unset REMOVE_NAME REMOVE_KEY REMOVE_CONFIG_PATH REMOVE_META_FILE REMOVE_ALLOWED REMOVE_ARCHIVE_PATH REMOVE_ERROR_CATEGORY TRANSACTION_FINALIZER REMOVE_ARCHIVE_SOURCES
|
||||
(( xtrace == 1 )) && set -x
|
||||
return "$status"
|
||||
}
|
||||
|
||||
main() {
|
||||
local status
|
||||
if (( $# == 0 )); then
|
||||
@@ -1565,7 +1854,11 @@ main() {
|
||||
version) print_version ;;
|
||||
status) load_client_model || return $?; print_status ;;
|
||||
list) load_client_model || return $?; print_list ;;
|
||||
remove|diagnose|logs|backups|restore|tui) run_placeholder "$1" ;;
|
||||
remove)
|
||||
shift
|
||||
(($# >= 1)) || { die "$EXIT_USAGE" 'Ожидается имя клиента'; return $?; }
|
||||
remove_client "$@" ;;
|
||||
diagnose|logs|backups|restore|tui) run_placeholder "$1" ;;
|
||||
*)
|
||||
if die "$EXIT_USAGE" "неизвестная команда: $1"; then status=0; else status=$?; fi
|
||||
print_usage >&2
|
||||
|
||||
Reference in New Issue
Block a user