diff --git a/README.md b/README.md index bbedf43..38af1c2 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ CLI-установка корпоративного VPN `vpn.lemanapro.ru` дл curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh ``` +Если установка запущена из терминала, скрипт сначала проверит, что уже стоит, и спросит по отсутствующим опциональным модулям. + После установки открой новый shell или выполни: ```sh @@ -52,6 +54,18 @@ curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh -s -- --dry-run ``` +Принудительно включить интерактивные вопросы: + +```sh +curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh -s -- --interactive +``` + +Запустить без вопросов, с выбранными флагами и дефолтами: + +```sh +curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh -s -- --non-interactive +``` + Если raw URL отличается, переопредели базовый адрес: ```sh @@ -103,6 +117,36 @@ JSON-режим тоже отдаёт модульный статус: vpn --status --json ``` +## Интерактивная установка + +Перед установкой `install.sh` печатает текущее состояние: + +```text +Detected state: + openconnect: yes + pipx: yes + openconnect-lite: yes + Bitwarden CLI: no + Touch ID helper: no + DNS cleanup: no + sudoers: no/no + shell aliases: no + Keychain password: no + Keychain TOTP seed: no +``` + +Если доступен терминал (`/dev/tty`), скрипт спросит только по тому, чего не хватает: + +- поставить ли Bitwarden CLI, если `bw` не найден; +- собрать ли Touch ID helper, если его нет и Bitwarden включён; +- настроить ли sudoers для `openconnect` и DNS cleanup; +- добавить ли алиасы в `~/.zshrc`; +- записать ли LDAP-пароль и TOTP seed в Keychain, если Bitwarden отключён. + +Флаги имеют приоритет над вопросами. Например, `--without-bitwarden` не будет спрашивать про Bitwarden, а `--no-shell` не будет предлагать алиасы. + +В неинтерактивной среде скрипт не задаёт вопросов и использует выбранные флаги/дефолты. Для CI или повторяемой установки лучше явно указывать `--non-interactive`. + ## Модули ### Core diff --git a/install.sh b/install.sh index dbf8b42..456b85c 100755 --- a/install.sh +++ b/install.sh @@ -18,6 +18,12 @@ INSTALL_ALIASES=1 CONFIGURE_KEYCHAIN=0 DRY_RUN=0 FORCE=0 +INTERACTIVE=auto +BITWARDEN_FORCED=0 +TOUCHID_FORCED=0 +SUDOERS_FORCED=0 +SHELL_FORCED=0 +CONFIGURE_KEYCHAIN_FORCED=0 usage() { cat <<'USAGE' @@ -35,6 +41,8 @@ Options: --raw-base-url URL Raw file base URL for curl installs --no-sudoers Do not install sudoers rules --no-shell Do not update ~/.zshrc aliases + --interactive Ask before installing optional missing modules + --non-interactive Use selected/default modules without prompts --minimal Same as --without-bitwarden --without-touchid --dry-run Print actions without changing files --force Reinstall files even when present @@ -49,11 +57,26 @@ USAGE while [ "$#" -gt 0 ]; do case "$1" in - --with-bitwarden) USE_BITWARDEN=1 ;; - --without-bitwarden) USE_BITWARDEN=0 ;; - --with-touchid) USE_TOUCHID=1 ;; - --without-touchid) USE_TOUCHID=0 ;; - --configure-keychain) CONFIGURE_KEYCHAIN=1 ;; + --with-bitwarden) + USE_BITWARDEN=1 + BITWARDEN_FORCED=1 + ;; + --without-bitwarden) + USE_BITWARDEN=0 + BITWARDEN_FORCED=1 + ;; + --with-touchid) + USE_TOUCHID=1 + TOUCHID_FORCED=1 + ;; + --without-touchid) + USE_TOUCHID=0 + TOUCHID_FORCED=1 + ;; + --configure-keychain) + CONFIGURE_KEYCHAIN=1 + CONFIGURE_KEYCHAIN_FORCED=1 + ;; --username) shift [ "$#" -gt 0 ] || { echo "--username requires a value" >&2; exit 1; } @@ -69,11 +92,21 @@ while [ "$#" -gt 0 ]; do [ "$#" -gt 0 ] || { echo "--raw-base-url requires a value" >&2; exit 1; } RAW_BASE_URL="${1%/}" ;; - --no-sudoers) INSTALL_SUDOERS=0 ;; - --no-shell) INSTALL_ALIASES=0 ;; + --no-sudoers) + INSTALL_SUDOERS=0 + SUDOERS_FORCED=1 + ;; + --no-shell) + INSTALL_ALIASES=0 + SHELL_FORCED=1 + ;; + --interactive) INTERACTIVE=1 ;; + --non-interactive) INTERACTIVE=0 ;; --minimal) USE_BITWARDEN=0 USE_TOUCHID=0 + BITWARDEN_FORCED=1 + TOUCHID_FORCED=1 ;; --dry-run) DRY_RUN=1 ;; --force) FORCE=1 ;; @@ -115,6 +148,136 @@ need_cmd() { command -v "$1" >/dev/null 2>&1 || die "Command not found: $1" } +has_tty() { + [ -r /dev/tty ] && [ -w /dev/tty ] +} + +interactive_enabled() { + case "$INTERACTIVE" in + 1) has_tty ;; + 0) return 1 ;; + auto) has_tty ;; + *) return 1 ;; + esac +} + +yes_no() { + prompt="$1" + default_answer="$2" + [ "$default_answer" = "y" ] || [ "$default_answer" = "n" ] || die "Invalid yes_no default: $default_answer" + + if ! interactive_enabled; then + [ "$default_answer" = "y" ] + return $? + fi + + if [ "$default_answer" = "y" ]; then + suffix="[Y/n]" + else + suffix="[y/N]" + fi + + while :; do + printf '%s %s ' "$prompt" "$suffix" > /dev/tty + IFS= read -r answer < /dev/tty || answer="" + case "$answer" in + "") [ "$default_answer" = "y" ]; return $? ;; + y|Y|yes|YES|Yes|д|Д|да|Да|ДА) return 0 ;; + n|N|no|NO|No|н|Н|нет|Нет|НЕТ) return 1 ;; + *) printf 'Введите y или n.\n' > /dev/tty ;; + esac + done +} + +bool_word() { + if "$@" >/dev/null 2>&1; then + printf 'yes' + else + printf 'no' + fi +} + +keychain_has() { + security find-generic-password -s "$1" -a "$2" >/dev/null 2>&1 +} + +zsh_aliases_installed() { + [ -f "$HOME/.zshrc" ] && grep -q '^# >>> lemana-vpn$' "$HOME/.zshrc" +} + +print_detected_state() { + log "Detected state:" + log " openconnect: $(bool_word command -v openconnect)" + log " pipx: $(bool_word command -v pipx)" + log " openconnect-lite: $(bool_word test -x "$HOME/.local/bin/openconnect-lite")" + log " Bitwarden CLI: $(bool_word command -v bw)" + log " Touch ID helper: $(bool_word test -x "$INSTALL_BIN_DIR/keychain-fingerprint")" + log " DNS cleanup: $(bool_word test -x "$DNS_CLEANUP")" + log " sudoers: $(bool_word test -f /etc/sudoers.d/lemana-vpn-openconnect)/$(bool_word test -f /etc/sudoers.d/lemana-vpn-dns)" + log " shell aliases: $(bool_word zsh_aliases_installed)" + log " Keychain password: $(bool_word keychain_has openconnect-lite "$USERNAME")" + log " Keychain TOTP seed: $(bool_word keychain_has openconnect-lite "totp/$USERNAME")" +} + +choose_modules() { + print_detected_state + + if ! interactive_enabled; then + log "Interactive prompts: off" + return 0 + fi + + log "Interactive prompts: on" + + if [ "$BITWARDEN_FORCED" -eq 0 ] && ! command -v bw >/dev/null 2>&1; then + if yes_no "Bitwarden CLI не найден. Поставить модуль Bitwarden?" y; then + USE_BITWARDEN=1 + else + USE_BITWARDEN=0 + fi + fi + + if [ "$TOUCHID_FORCED" -eq 0 ]; then + if [ "$USE_BITWARDEN" -eq 1 ]; then + if ! [ -x "$INSTALL_BIN_DIR/keychain-fingerprint" ]; then + if yes_no "Touch ID helper не найден. Собрать и установить?" y; then + USE_TOUCHID=1 + else + USE_TOUCHID=0 + fi + fi + else + USE_TOUCHID=0 + fi + fi + + if [ "$SUDOERS_FORCED" -eq 0 ]; then + if ! [ -f /etc/sudoers.d/lemana-vpn-openconnect ] || ! [ -f /etc/sudoers.d/lemana-vpn-dns ]; then + if yes_no "Настроить sudoers для VPN/DNS без повторного sudo-пароля?" y; then + INSTALL_SUDOERS=1 + else + INSTALL_SUDOERS=0 + fi + fi + fi + + if [ "$SHELL_FORCED" -eq 0 ] && ! zsh_aliases_installed; then + if yes_no "Добавить алиасы vpn/vpn-debug/vpn-fix-dns в ~/.zshrc?" y; then + INSTALL_ALIASES=1 + else + INSTALL_ALIASES=0 + fi + fi + + if [ "$CONFIGURE_KEYCHAIN_FORCED" -eq 0 ] && [ "$USE_BITWARDEN" -eq 0 ]; then + if ! keychain_has openconnect-lite "$USERNAME" || ! keychain_has openconnect-lite "totp/$USERNAME"; then + if yes_no "Bitwarden отключён, а Keychain credentials неполные. Записать LDAP-пароль и TOTP seed после установки?" y; then + CONFIGURE_KEYCHAIN=1 + fi + fi + fi +} + script_dir() { case "$0" in */*) cd "$(dirname "$0")" 2>/dev/null && pwd ;; @@ -325,6 +488,8 @@ main() { tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT INT TERM + choose_modules + log "Installing Lemana VPN" log "Modules: bitwarden=$USE_BITWARDEN touchid=$USE_TOUCHID sudoers=$INSTALL_SUDOERS shell=$INSTALL_ALIASES" diff --git a/tests/smoke.sh b/tests/smoke.sh new file mode 100755 index 0000000..14b45b1 --- /dev/null +++ b/tests/smoke.sh @@ -0,0 +1,22 @@ +#!/bin/sh +set -eu + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT INT TERM + +export HOME="$TMP_DIR/home" +export LEMANA_VPN_BIN_DIR="$HOME/bin" +export LEMANA_VPN_CONFIG_DIR="$HOME/.config/lemana-vpn" +export OPENCONNECT_LITE_CONFIG_DIR="$HOME/.config/openconnect-lite" +mkdir -p "$HOME" + +output="$(cd "$ROOT" && sh install.sh --dry-run --non-interactive --minimal)" + +printf '%s\n' "$output" | grep -q 'Detected state:' +printf '%s\n' "$output" | grep -q 'Interactive prompts: off' +printf '%s\n' "$output" | grep -q 'Modules: bitwarden=0 touchid=0 sudoers=1 shell=1' +printf '%s\n' "$output" | grep -q 'sudo install -d -m 755 -o root -g wheel /usr/local/sbin' + +printf 'smoke ok\n' +