diff --git a/demo.sh b/demo.sh deleted file mode 100644 index dafbb05..0000000 --- a/demo.sh +++ /dev/null @@ -1,1084 +0,0 @@ -#!/usr/bin/env bash -# ============================================================================= -# demo-setup.sh v5 — помощник настройки учебного стенда ДЭ 09.02.06 -# «Сетевое и системное администрирование». Целевая ОС: RED OS 8. -# Написан заново: NetworkManager (nmcli), firewalld, SELinux остаются включены. -# -# bash demo-setup.sh справка, ничего не меняет -# bash demo-setup.sh --dry-run меню; команды только показываются -# sudo bash demo-setup.sh --apply меню; команды выполняются -# -# Скрипт копируется на каждую ВМ вместе с demo-setup.conf. На ВМ выбираете -# её машину, затем отдельный пункт или весь модуль (М1/М2/М3). -# Перед применением сделайте снимок ВМ и работайте из консоли гипервизора. -# ============================================================================= -set -o pipefail -VERSION=4.0 -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -CONF="${DEMO_CONF:-$SCRIPT_DIR/demo-setup.conf}" -LOG=/var/log/demo-setup.log -MODE=help -case ${1:-} in - --apply) MODE=apply ;; --dry-run|-n) MODE=dry ;; -h|--help|"") MODE=help ;; - *) echo "Неизвестный параметр: $1 (см. --help)" >&2; exit 2 ;; -esac -DRY=0; [[ $MODE == dry ]] && DRY=1 - -# ------------------------- параметры по умолчанию ---------------------------- -DOMAIN=au-team.irpo -TZ_NAME=Asia/Yekaterinburg -SSH_USER=sshuser; SSH_UID=2027; SSH_PORT=2027 -ADM_USER=net_admin -BANNER_TEXT='Authorized access only' -DNS_FWD=77.88.8.7; DNS_FWD2=77.88.8.3 -NTP_UPSTREAM=pool.ntp.org -ISO_SRC=/dev/sr0; ISO_DIR=/mnt/additional -AD_WORKGROUP=AU-TEAM; AD_ADMIN=Administrator -CLI_SSH_USER=sshuser; CLI_SSH_PORT=22 -RAID_LEVEL=0; RAID_MOUNT=/raid -APP_INNER_PORT=8000 -CERT_ALG=rsa -# WAN -ISP_HQ_IP=172.16.1.1; HQ_RTR_WAN_IP=172.16.1.2; HQ_WAN_PFX=28 -ISP_BR_IP=172.16.2.1; BR_RTR_WAN_IP=172.16.2.2; BR_WAN_PFX=28 -# HQ -VLAN_SRV=100; VLAN_CLI=200; VLAN_MGMT=999 -SRV_NET=192.168.100.0; SRV_PFX=27; HQ_RTR_SRV_IP=192.168.100.1; HQ_SRV_IP=192.168.100.2 -CLI_NET=192.168.200.0; CLI_PFX=28; HQ_RTR_CLI_IP=192.168.200.1 -DHCP_START=192.168.200.2; DHCP_END=192.168.200.14; HQ_CLI_IP=192.168.200.2; HQ_CLI_MAC= -MGMT_NET=192.168.99.0; MGMT_PFX=29; HQ_RTR_MGMT_IP=192.168.99.1; HQ_SW_MGMT_IP=192.168.99.2 -# BR -FW_NET=192.168.10.0; FW_PFX=30; BR_RTR_FW_IP=192.168.10.1; BR_FW_IP=192.168.10.2 -BR_NET=192.168.20.0; BR_PFX=28; BR_FW_LAN_IP=192.168.20.1; BR_SRV_IP=192.168.20.2 -# GRE / OSPF -GRE_IF=gre1; GRE_NET=10.10.10.0; GRE_PFX=30; HQ_GRE_IP=10.10.10.1; BR_GRE_IP=10.10.10.2 -OSPF_FW_AUTH=no -# DNS: имя=IP или имя=IP:ptr через пробел (пусто = по умолчанию) -DNS_RECORDS= -WEB_DNS_IP=; DOCKER_DNS_IP= -# Интерфейсы (запрашиваются при первой настройке) -ISP_WAN_IF=; ISP_HQ_IF=; ISP_BR_IF= -HQ_RTR_WAN_IF=; HQ_RTR_LAN_IF=; BR_RTR_WAN_IF=; BR_RTR_LAN_IF= -BR_FW_WAN_IF=; BR_FW_LAN_IF=; HQ_SW_UP_IF=; HQ_SW_SRV_IF=; HQ_SW_CLI_IF= -HQ_SRV_IF=; BR_SRV_IF=; HQ_CLI_IF= - -# Описание параметров: ПЕРЕМЕННАЯ|тип|подпись. Типы: ip ipo pfx port int name text yn alg list if -PARAMS=( -"DOMAIN|name|Домен" "TZ_NAME|text|Часовой пояс" "SSH_USER|name|Пользователь серверов" -"SSH_UID|int|UID пользователя серверов" "SSH_PORT|port|Порт SSH серверов" "ADM_USER|name|Пользователь маршрутизаторов" -"BANNER_TEXT|text|Баннер SSH" "DNS_FWD|ip|DNS-форвардер 1" "DNS_FWD2|ipo|DNS-форвардер 2 (можно пусто)" -"NTP_UPSTREAM|text|Внешний NTP для ISP (пусто = нет)" "ISO_SRC|text|Additional.iso: устройство или файл" "ISO_DIR|text|Точка монтирования ISO" -"AD_WORKGROUP|name|NetBIOS-имя домена" "AD_ADMIN|name|Администратор домена" -"CLI_SSH_USER|name|Пользователь HQ-CLI для Ansible" "CLI_SSH_PORT|port|Порт SSH HQ-CLI" -"RAID_LEVEL|int|Уровень RAID" "RAID_MOUNT|text|Точка монтирования RAID" "APP_INNER_PORT|port|Внутренний порт testapp" "CERT_ALG|alg|Сертификаты (rsa/gost)" -"ISP_HQ_IP|ip|ISP: IP в сторону HQ" "HQ_RTR_WAN_IP|ip|HQ-RTR: WAN IP" "HQ_WAN_PFX|pfx|Префикс ISP–HQ" -"ISP_BR_IP|ip|ISP: IP в сторону BR" "BR_RTR_WAN_IP|ip|BR-RTR: WAN IP" "BR_WAN_PFX|pfx|Префикс ISP–BR" -"VLAN_SRV|int|VLAN серверов" "VLAN_CLI|int|VLAN клиентов" "VLAN_MGMT|int|VLAN управления" -"SRV_NET|ip|Сеть серверов" "SRV_PFX|pfx|Префикс сети серверов" "HQ_RTR_SRV_IP|ip|HQ-RTR в VLAN серверов" "HQ_SRV_IP|ip|HQ-SRV" -"CLI_NET|ip|Сеть клиентов" "CLI_PFX|pfx|Префикс сети клиентов" "HQ_RTR_CLI_IP|ip|HQ-RTR в VLAN клиентов" -"DHCP_START|ip|Начало DHCP" "DHCP_END|ip|Конец DHCP" "HQ_CLI_IP|ip|HQ-CLI (DNS/Ansible)" "HQ_CLI_MAC|text|MAC HQ-CLI для резерва (пусто = нет)" -"MGMT_NET|ip|Сеть управления" "MGMT_PFX|pfx|Префикс управления" "HQ_RTR_MGMT_IP|ip|HQ-RTR в VLAN управления" "HQ_SW_MGMT_IP|ip|HQ-SW управление" -"FW_NET|ip|Сеть BR-RTR–BR-FW" "FW_PFX|pfx|Префикс BR-RTR–BR-FW" "BR_RTR_FW_IP|ip|BR-RTR к BR-FW" "BR_FW_IP|ip|BR-FW к BR-RTR" -"BR_NET|ip|Сеть BR-SRV" "BR_PFX|pfx|Префикс сети BR-SRV" "BR_FW_LAN_IP|ip|BR-FW в сети BR-SRV" "BR_SRV_IP|ip|BR-SRV" -"GRE_IF|name|Имя GRE-интерфейса" "GRE_NET|ip|Сеть GRE" "GRE_PFX|pfx|Префикс GRE" "HQ_GRE_IP|ip|HQ-RTR в туннеле" "BR_GRE_IP|ip|BR-RTR в туннеле" -"OSPF_FW_AUTH|yn|MD5 OSPF и на линке BR-FW (yes/no)" "DNS_RECORDS|list|DNS: имя=IP[:ptr] через пробел (пусто = по умолчанию)" -"WEB_DNS_IP|ipo|IP для web (пусто = ISP_BR_IP)" "DOCKER_DNS_IP|ipo|IP для docker (пусто = ISP_HQ_IP)" -) -IFACE_VARS=(ISP_WAN_IF ISP_HQ_IF ISP_BR_IF HQ_RTR_WAN_IF HQ_RTR_LAN_IF BR_RTR_WAN_IF BR_RTR_LAN_IF BR_FW_WAN_IF BR_FW_LAN_IF HQ_SW_UP_IF HQ_SW_SRV_IF HQ_SW_CLI_IF HQ_SRV_IF BR_SRV_IF HQ_CLI_IF) - -# ------------------------------- вывод --------------------------------------- -if [[ -t 1 ]]; then C_R=$'\e[31m'; C_G=$'\e[32m'; C_Y=$'\e[33m'; C_C=$'\e[36m'; C_B=$'\e[1m'; C_0=$'\e[0m'; else C_R=; C_G=; C_Y=; C_C=; C_B=; C_0=; fi -msg(){ echo "${C_C}==>${C_0} $*"; } -ok(){ echo "${C_G}[OK]${C_0} $*"; } -warn(){ echo "${C_Y}[!]${C_0} $*"; } -err(){ echo "${C_R}[ОШИБКА]${C_0} $*" >&2; } -hdr(){ echo; echo "${C_B}===== $* =====${C_0}"; } -have(){ command -v "$1" >/dev/null 2>&1; } -exec 5<&0 # исходный ввод (клавиатура) — для окон внутри конвейеров -pause(){ read -r -p "Enter — продолжить..." _ <&5 || true; } -yesno(){ ui_yesno "$@"; } - -usage(){ cat <&2; exit 1; } - [[ -t 0 ]] || { echo "--apply запускается только из интерактивной консоли." >&2; exit 1; } - grep -qi 'red' /etc/os-release 2>/dev/null || warn "Похоже, это не RED OS — скрипт рассчитан на RED OS 8." - touch "$LOG" && chmod 600 "$LOG" && exec > >(tee -a "$LOG") 2>&1 -else - warn "РЕЖИМ ПРОСМОТРА: изменяющие команды только выводятся (строки с '+')." -fi - -# --------------------------- выполнение команд ------------------------------- -# run КОМАНДА... — в режиме просмотра печатает команду, иначе выполняет. -exec 3>&1 -run(){ if ((DRY)); then { printf '%s+' "$C_C"; printf ' %q' "$@"; printf '%s\n' "$C_0"; } >&3; return 0; fi; "$@"; } -# put ФАЙЛ [ПРАВА] < "$f.demo-tmp") && chmod "$m" "$f.demo-tmp" && mv -f "$f.demo-tmp" "$f"; } -# backup ФАЙЛ — один раз сохраняет исходный файл как ФАЙЛ.demo-orig. -backup(){ [[ -f $1 && ! -e $1.demo-orig ]] && { ((DRY)) || cp -a "$1" "$1.demo-orig"; }; return 0; } -# set_kv ФАЙЛ КЛЮЧ ЗНАЧЕНИЕ — заменяет или добавляет строку «КЛЮЧ=ЗНАЧЕНИЕ». -set_kv(){ local f=$1 k=$2 v=$3 - if ((DRY)); then echo "${C_C}+ $f: $k=$v${C_0}"; return 0; fi - backup "$f"; touch "$f" || return 1 - if grep -qE "^[#[:space:]]*$k=" "$f"; then sed -i -E "s|^[#[:space:]]*$k=.*|$k=$v|" "$f"; else echo "$k=$v" >> "$f"; fi; } -pkg(){ local p missing=(); for p in "$@"; do rpm -q "$p" >/dev/null 2>&1 || missing+=("$p"); done - ((${#missing[@]})) || return 0; msg "Установка пакетов: ${missing[*]}" - run dnf install -y "${missing[@]}" || { err "Не удалось установить: ${missing[*]}"; return 1; }; } -# pkg_any "пакеты варианта 1" "пакеты варианта 2" ... — первый установившийся вариант. -pkg_any(){ local v; for v in "$@"; do read -ra P <<< "$v"; pkg "${P[@]}" 2>/dev/null && return 0; done; err "Ни один вариант пакетов не установился"; return 1; } -svc(){ local s; for s in "$@"; do if ((DRY)) || systemctl cat "$s" >/dev/null 2>&1; then - run systemctl enable "$s" && run systemctl restart "$s" && { ok "Служба $s запущена"; return 0; }; fi; done - err "Не удалось запустить службу: $*"; return 1; } - -# ------------------------------- проверки ------------------------------------ -is_ip(){ local IFS=. o; [[ $1 =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || return 1; for o in $1; do ((10#$o <= 255)) || return 1; done; } -is_pfx(){ [[ $1 =~ ^([0-9]|[12][0-9]|3[0-2])$ ]]; } -is_port(){ [[ $1 =~ ^[0-9]{1,5}$ ]] && (($1 >= 1 && $1 <= 65535)); } -is_name(){ [[ $1 =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; } -is_iface(){ [[ $1 =~ ^[A-Za-z0-9._@-]{1,15}$ && -e /sys/class/net/$1 ]]; } -valid(){ local t=$1 v=$2 - case $t in - ip) is_ip "$v" ;; ipo) [[ -z $v ]] || is_ip "$v" ;; pfx) is_pfx "$v" ;; port) is_port "$v" ;; - int) [[ $v =~ ^[0-9]{1,6}$ ]] ;; name) is_name "$v" ;; yn) [[ $v == yes || $v == no ]] ;; alg) [[ $v == rsa || $v == gost ]] ;; - if) is_iface "$v" ;; text|list) [[ $v =~ ^[A-Za-z0-9_./:@,+\ =-]*$ ]] ;; *) return 1 ;; - esac; } -pfx2mask(){ local p=$1 m="" i b; is_pfx "$p" || return 1 - for i in 1 2 3 4; do b=$(( p >= 8 ? 8 : (p > 0 ? p : 0) )); m+=$(( 256 - (1 << (8 - b)) )); ((i < 4)) && m+=.; p=$(( p - 8 )); done; echo "$m"; } -rev_zone(){ local IFS=.; set -- $1; echo "$3.$2.$1"; } -fqdn(){ echo "$1.$DOMAIN"; } -REALM(){ echo "${DOMAIN^^}"; } -param_type(){ local e; for e in "${PARAMS[@]}"; do [[ ${e%%|*} == "$1" ]] && { e=${e#*|}; echo "${e%%|*}"; return; }; done - local i; for i in "${IFACE_VARS[@]}"; do [[ $i == "$1" ]] && { echo if; return; }; done; } - -# ------------------------------- параметры ----------------------------------- -conf_load(){ [[ -f $CONF ]] || return 0 - if [[ $MODE == apply ]]; then - [[ $(stat -c '%u %a' "$CONF") == "0 600" ]] || { err "$CONF должен принадлежать root и иметь права 600 (chown root: и chmod 600)"; exit 1; } - fi - local line k v t n=0 - while IFS= read -r line || [[ -n $line ]]; do n=$((n+1)) - [[ -z $line || $line == \#* ]] && continue - k=${line%%=*}; v=${line#*=} - t=$(param_type "$k"); [[ -n $t ]] || { err "$CONF:$n: неизвестный параметр $k"; exit 1; } - [[ $t == if ]] && { [[ -z $v || $v =~ ^[A-Za-z0-9._@-]{1,15}$ ]] || { err "$CONF:$n: плохое имя интерфейса"; exit 1; }; printf -v "$k" '%s' "$v"; continue; } - valid "$t" "$v" || { err "$CONF:$n: недопустимое значение $k"; exit 1; } - printf -v "$k" '%s' "$v" - done < "$CONF"; } -conf_save(){ local e k tmp - if ((DRY)); then return 0; fi - tmp=$(mktemp "$CONF.XXXX") || return 1 - { echo "# demo-setup v$VERSION — параметры стенда (без паролей)" - for e in "${PARAMS[@]}"; do k=${e%%|*}; echo "$k=${!k}"; done - for k in "${IFACE_VARS[@]}"; do echo "$k=${!k}"; done; } > "$tmp" - chmod 600 "$tmp" && mv -f "$tmp" "$CONF"; } -PASS= -over_ssh_warn(){ [[ -n ${SSH_CONNECTION:-} ]] && warn "Вы подключены по SSH: изменение сети/SSH может оборвать сеанс. Лучше работать из консоли."; return 0; } -conf_load - -# ============================== сеть (nmcli) ================================= -nm_ready(){ ((DRY)) && return 0; systemctl is-active --quiet NetworkManager || { err "NetworkManager не запущен"; return 1; }; } -nm_has(){ nmcli -t -f NAME con show 2>/dev/null | grep -Fxq "$1"; } -# Отключает автоподключение чужих профилей интерфейса (не удаляет их). -nm_quiet_others(){ local ifc=$1 keep=$2 line name dev - while IFS=: read -r name dev; do - [[ $name == "$keep" || -z $name ]] && continue - [[ $dev == "$ifc" || $(nmcli -g connection.interface-name con show "$name" 2>/dev/null) == "$ifc" ]] || continue - warn "Профиль «$name» на $ifc: автоподключение будет выключено (профиль не удаляется)" - run nmcli con mod "$name" connection.autoconnect no || return 1 - done < <(nmcli -t -f NAME,DEVICE con show 2>/dev/null); } -# nm_set ПРОФИЛЬ "аргументы add" ИНТЕРФЕЙС_ДЛЯ_ЧУЖИХ режим адрес шлюз dns -# режим: static | dhcp | none (без IP — порт моста/родитель VLAN) -nm_set(){ local c=$1 addargs=$2 ifc=$3 mode=$4 addr=${5-} gw=${6-} dns=${7-} rc - nm_ready || return 1 - [[ -n $ifc ]] && { nm_quiet_others "$ifc" "$c" || return 1; } - if ! nm_has "$c"; then read -ra A <<< "$addargs"; run nmcli con add con-name "$c" "${A[@]}" || { err "nmcli: не создан профиль $c"; return 1; }; fi - case $mode in - static) run nmcli con mod "$c" ipv4.method manual ipv4.addresses "$addr" ipv4.gateway "$gw" ipv4.dns "$dns" ipv4.ignore-auto-dns yes ;; - dhcp) run nmcli con mod "$c" ipv4.method auto ipv4.addresses "" ipv4.gateway "" ipv4.dns "" ipv4.ignore-auto-dns no ;; - none) run nmcli con mod "$c" ipv4.method disabled ipv4.addresses "" ipv4.gateway "" ;; - esac; rc=$? - ((rc == 0)) && run nmcli con mod "$c" ipv6.method disabled connection.autoconnect yes; rc=$((rc + $?)) - ((rc == 0)) || { err "nmcli: не удалось изменить профиль $c"; return 1; } - run nmcli con up "$c" || { err "nmcli: профиль $c не поднялся"; return 1; } - ok "Профиль $c: $mode ${addr:-}"; } -net_eth(){ nm_set "demo-$1" "type ethernet ifname $1" "$1" "${@:2}"; } -net_vlan(){ local p=$1 vid=$2; shift 2; nm_set "demo-$p.$vid" "type vlan ifname $p.$vid dev $p id $vid" "" "$@"; } -net_gre(){ local l=$1 r=$2 addr=$3 - nm_set "demo-$GRE_IF" "type ip-tunnel ifname $GRE_IF mode gre local $l remote $r" "" static "$addr" || return 1 - run nmcli con mod "demo-$GRE_IF" ip-tunnel.ttl 64 && run nmcli con up "demo-$GRE_IF"; } -set_zone(){ local c=$1 z=$2 ifc=$3 - ((DRY)) || systemctl is-active --quiet firewalld || return 0 - run nmcli con mod "$c" connection.zone "$z" && run firewall-cmd --zone="$z" --change-interface="$ifc" >/dev/null && ok "$ifc → зона $z"; } -enable_forward(){ put /etc/sysctl.d/90-demo-forward.conf <<< 'net.ipv4.ip_forward = 1' && run sysctl -p /etc/sysctl.d/90-demo-forward.conf >/dev/null && ok "Маршрутизация IPv4 включена"; } - -# ============================== firewalld ==================================== -fw_on(){ ((DRY)) && return 0; systemctl is-active --quiet firewalld && return 0 - warn "firewalld не запущен."; yesno "Включить firewalld (нужен для NAT/DNAT)?" y || return 1 - pkg firewalld && run systemctl enable --now firewalld; } -fw(){ run firewall-cmd --permanent "$@" >/dev/null || { err "firewall-cmd $*"; return 1; }; } -fw_reload(){ run firewall-cmd --reload >/dev/null && ok "firewalld перезагружен"; } -# fw_open сервис|порт/proto ... — открывает на серверах в зоне по умолчанию (если firewalld активен). -fw_open(){ ((DRY)) || systemctl is-active --quiet firewalld || return 0; local x - for x in "$@"; do if [[ $x == */* ]]; then fw --add-port="$x"; else fw --add-service="$x" 2>/dev/null || warn "В firewalld нет сервиса $x"; fi; done - fw_reload; } - -# ============================== общие пункты ================================= -MACHINE= -c_hostname(){ local h; h=$(fqdn "$MACHINE"); run hostnamectl set-hostname "$h" || return 1; ok "Имя хоста: $h"; } -c_tz(){ run timedatectl set-timezone "$TZ_NAME" || return 1; ok "Часовой пояс: $TZ_NAME"; date; } -sudo_nopass(){ local u=$1 f="/etc/sudoers.d/demo-$1" - pkg sudo || return 1 - put "$f" 440 <<< "$u ALL=(ALL:ALL) NOPASSWD: ALL" || return 1 - ((DRY)) || visudo -cf "$f" >/dev/null || { err "Ошибка sudoers — файл удалён"; rm -f "$f"; return 1; } - ok "sudo без пароля: $u"; } -# mkuser ИМЯ [UID] — создаёт пользователя; существующего не трогает без согласия. -mkuser(){ local u=$1 uid=${2-}; need_pass || return 1 - if id "$u" >/dev/null 2>&1; then - warn "Пользователь $u уже существует ($(id "$u"))." - yesno "Задать ему общий пароль и sudo?" n || return 0 - else run useradd -m -s /bin/bash ${uid:+-u "$uid"} "$u" || return 1; fi - if ((DRY)); then echo "${C_C}+ chpasswd ($u:***)${C_0}"; else printf '%s:%s\n' "$u" "$PASS" | chpasswd || return 1; fi - sudo_nopass "$u"; ok "Пользователь $u готов"; } -u_srv(){ mkuser "$SSH_USER" "$SSH_UID"; } -u_rtr(){ mkuser "$ADM_USER"; } -selinux_on(){ have getenforce && [[ $(getenforce) == Enforcing ]]; } -se_port(){ selinux_on || return 0; pkg policycoreutils-python-utils || return 1 - run semanage port -a -t "$1" -p tcp "$2" 2>/dev/null || run semanage port -m -t "$1" -p tcp "$2" || return 1; ok "SELinux: tcp/$2 → $1"; } -sshd_include(){ local c=/etc/ssh/sshd_config - grep -qE '^[[:space:]]*Include[[:space:]]+/etc/ssh/sshd_config.d/\*\.conf' "$c" 2>/dev/null && return 0 - ((DRY)) && { echo "${C_C}+ добавить Include sshd_config.d в начало $c${C_0}"; return 0; } - backup "$c"; sed -i '1i Include /etc/ssh/sshd_config.d/*.conf' "$c"; } -sshd_apply(){ local port=$1 - # Нет ключей хоста (свежий клон шаблона после очистки) — создаём, иначе sshd -t падает - ls /etc/ssh/ssh_host_*_key >/dev/null 2>&1 || run ssh-keygen -A || return 1 - ((DRY)) || sshd -t || { err "sshd -t: ошибка конфигурации, служба не перезапущена"; return 1; } - [[ $port != 22 ]] && { se_port ssh_port_t "$port" || return 1; } - fw_open "$port/tcp" - over_ssh_warn; run systemctl enable sshd && run systemctl restart sshd || return 1 - ((DRY)) || ss -tln | grep -q ":$port " || { err "sshd не слушает порт $port"; return 1; } - ok "SSH слушает порт $port"; } -# Защищённый SSH серверов: порт, только SSH_USER, 2 попытки, баннер. -c_sshd(){ pkg openssh-server || return 1; sshd_include || return 1 - put /etc/ssh/demo-banner <<< "$BANNER_TEXT" || return 1 - put /etc/ssh/sshd_config.d/10-demo.conf 600 <> "$f"; fi - svc chronyd || return 1; ((DRY)) || { sleep 3; chronyc sources; }; ok "NTP-клиент → $srv"; } -mount_iso(){ ((DRY)) && return 0; mountpoint -q "$ISO_DIR" && return 0; mkdir -p "$ISO_DIR" - if [[ -f $ISO_SRC ]]; then mount -o loop,ro "$ISO_SRC" "$ISO_DIR"; else mount -o ro "$ISO_SRC" "$ISO_DIR"; fi 2>/dev/null \ - && ok "ISO смонтирован в $ISO_DIR" || { err "Не смонтирован $ISO_SRC — подключите Additional.iso или измените ISO_SRC"; return 1; }; } -iso_find(){ mount_iso || return 1; ((DRY)) && { echo "$ISO_DIR/$1"; return 0; }; find "$ISO_DIR" -iname "$1" 2>/dev/null | head -1; } -os_prep(){ pkg_any "bind-utils tcpdump traceroute curl wget tar nano policycoreutils-python-utils" "bind-utils curl tar nano" || return 1 - echo "SELinux: $(getenforce 2>/dev/null || echo нет) firewalld: $(systemctl is-active firewalld 2>/dev/null)" - ok "Утилиты установлены (firewalld и SELinux не отключаются)"; } - -# frr_ospf ROUTER_ID "интерфейсы с MD5" "интерфейсы без MD5" СЕТЬ... -frr_ospf(){ local rid=$1 auth=$2 plain=$3 i n; shift 3; pkg frr || return 1; need_pass || return 1 - set_kv /etc/frr/daemons ospfd yes || return 1 - local key=${PASS:0:16} - { echo "frr defaults traditional"; echo "hostname ${MACHINE:-$(hostname -s 2>/dev/null)}"; echo "log syslog informational"; echo "!" - for i in $auth; do echo "interface $i"; echo " ip ospf network point-to-point"; echo " ip ospf mtu-ignore" - echo " ip ospf authentication message-digest"; echo " ip ospf message-digest-key 1 md5 $key"; echo "!"; done - for i in $plain; do echo "interface $i"; echo " ip ospf network point-to-point"; echo "!"; done - echo "router ospf"; echo " ospf router-id $rid"; echo " passive-interface default" - for i in $auth $plain; do echo " no passive-interface $i"; done - for n in "$@"; do echo " network $n area 0"; done; echo "!"; } | put /etc/frr/frr.conf 640 || return 1 - ((DRY)) || chown frr:frr /etc/frr/frr.conf 2>/dev/null - svc frr || return 1 - ok "OSPF: router-id $rid; ключ MD5 = первые 16 символов общего пароля" - ((DRY)) || { sleep 5; vtysh -c 'show ip ospf neighbor'; } - warn "Соседство появится, когда OSPF настроен на обоих концах."; } - -# =================================== ISP ===================================== -isp_net(){ over_ssh_warn - need_iface ISP_WAN_IF "Интерфейс в Интернет (DHCP)"; need_iface ISP_HQ_IF "Интерфейс к HQ-RTR"; need_iface ISP_BR_IF "Интерфейс к BR-RTR" - net_eth "$ISP_WAN_IF" dhcp || return 1 - net_eth "$ISP_HQ_IF" static "$ISP_HQ_IP/$HQ_WAN_PFX" || return 1 - net_eth "$ISP_BR_IF" static "$ISP_BR_IP/$BR_WAN_PFX" || return 1 - enable_forward; } -isp_nat(){ fw_on || return 1 - set_zone "demo-$ISP_WAN_IF" external "$ISP_WAN_IF" && set_zone "demo-$ISP_HQ_IF" trusted "$ISP_HQ_IF" && set_zone "demo-$ISP_BR_IF" trusted "$ISP_BR_IF" || return 1 - fw --zone=external --add-masquerade && fw_reload && enable_forward && ok "NAT: $ISP_WAN_IF (зона external, masquerade)"; } -isp_chrony(){ pkg chrony || return 1 - { echo "# demo-setup: NTP-сервер ISP"; [[ -n $NTP_UPSTREAM ]] && echo "server $NTP_UPSTREAM iburst" - echo "local stratum 5"; echo "allow $ISP_HQ_IP/$HQ_WAN_PFX"; echo "allow $ISP_BR_IP/$BR_WAN_PFX" - echo "driftfile /var/lib/chrony/drift"; echo "makestep 1.0 3"; echo "rtcsync"; } | put /etc/chrony.conf || return 1 - svc chronyd && ok "chrony: сервер stratum 5 для HQ и BR"; } -ngx_server(){ # ngx_server ИМЯ БЭКЕНД https(yes/no) auth(yes/no) - local n=$1 b=$2 s=$3 a=$4 - echo "server {" - if [[ $s == yes ]]; then echo " listen 443 ssl;"; echo " ssl_certificate /etc/nginx/ssl/$n.crt;"; echo " ssl_certificate_key /etc/nginx/ssl/$n.key;" - else echo " listen 80;"; fi - echo " server_name $(fqdn "$n");"; echo " location / {"; echo " proxy_pass http://$b;" - echo " proxy_set_header Host \$host;"; echo " proxy_set_header X-Real-IP \$remote_addr;" - echo " proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;" - [[ $a == yes ]] && { echo " auth_basic \"Restricted\";"; echo " auth_basic_user_file /etc/nginx/.htpasswd;"; } - echo " }"; echo "}" - [[ $s == yes ]] && echo "server { listen 80; server_name $(fqdn "$n"); return 301 https://\$host\$request_uri; }"; return 0; } -ngx_write(){ local s=no a=no; [[ -f /etc/nginx/ssl/web.crt ]] && s=yes; [[ -f /etc/nginx/.htpasswd ]] && a=yes - pkg nginx || return 1 - { echo "# demo-setup: обратный прокси web/docker"; ngx_server web "$HQ_RTR_WAN_IP:8080" "$s" "$a"; ngx_server docker "$BR_RTR_WAN_IP:8080" "$s" no; } \ - | put /etc/nginx/conf.d/demo-proxy.conf || return 1 - selinux_on && { run setsebool -P httpd_can_network_connect 1 || return 1; } - ((DRY)) || nginx -t || { err "nginx -t: ошибка конфигурации"; return 1; } - svc nginx && ok "nginx: web → $HQ_RTR_WAN_IP:8080, docker → $BR_RTR_WAN_IP:8080 (HTTPS: $s, auth: $a)"; } -isp_proxy(){ ngx_write; } -isp_auth(){ need_pass || return 1; pkg nginx openssl || return 1 - local h; h=$(openssl passwd -apr1 -stdin <<< "$PASS") || return 1 - put /etc/nginx/.htpasswd 640 <<< "WEB:$h" || return 1; ((DRY)) || chgrp nginx /etc/nginx/.htpasswd - ok "Basic-аутентификация: пользователь WEB"; ngx_write; } -isp_https(){ run mkdir -p /etc/nginx/ssl || return 1 - msg "Копирую сертификаты с HQ-SRV через DNAT $HQ_RTR_WAN_IP:$SSH_PORT ($SSH_USER, нужен пароль)" - run scp -P "$SSH_PORT" -o StrictHostKeyChecking=accept-new "$SSH_USER@$HQ_RTR_WAN_IP:certs/{web,docker}.{crt,key}" /etc/nginx/ssl/ \ - || { err "Не скопировано. Положите web.crt web.key docker.crt docker.key в /etc/nginx/ssl вручную"; return 1; } - run chmod 600 /etc/nginx/ssl/web.key /etc/nginx/ssl/docker.key && ngx_write; } -chk_isp(){ ip -br a; ip r; firewall-cmd --get-active-zones 2>/dev/null; chronyc tracking 2>/dev/null | head -3 - systemctl is-active nginx chronyd 2>/dev/null; ping -c2 -W1 "$HQ_RTR_WAN_IP"; ping -c2 -W1 "$BR_RTR_WAN_IP"; } -M_isp=( -"М1|Сеть: Интернет по DHCP, адреса к HQ и BR|isp_net" "М1|NAT в Интернет (firewalld)|isp_nat" "М1|Часовой пояс|c_tz" -"М2|NTP-сервер chrony (stratum 5)|isp_chrony" "М3|nginx: обратный прокси web и docker|isp_proxy" -"М3|Basic-аутентификация WEB для web|isp_auth" "М3|HTTPS (сертификаты с HQ-SRV)|isp_https") - -# ================================= HQ-RTR ==================================== -hqr_net(){ over_ssh_warn - need_iface HQ_RTR_WAN_IF "WAN-интерфейс (к ISP)"; need_iface HQ_RTR_LAN_IF "LAN-интерфейс (транк к HQ-SW)" - net_eth "$HQ_RTR_WAN_IF" static "$HQ_RTR_WAN_IP/$HQ_WAN_PFX" "$ISP_HQ_IP" "$DNS_FWD" || return 1 - net_eth "$HQ_RTR_LAN_IF" none || return 1 - net_vlan "$HQ_RTR_LAN_IF" "$VLAN_SRV" static "$HQ_RTR_SRV_IP/$SRV_PFX" || return 1 - net_vlan "$HQ_RTR_LAN_IF" "$VLAN_CLI" static "$HQ_RTR_CLI_IP/$CLI_PFX" || return 1 - net_vlan "$HQ_RTR_LAN_IF" "$VLAN_MGMT" static "$HQ_RTR_MGMT_IP/$MGMT_PFX" || return 1 - enable_forward; } -rtr_zones(){ local wan=$1 lan=$2 v; fw_on || return 1 - set_zone "demo-$wan" external "$wan" || return 1 - [[ -n $lan ]] && { set_zone "demo-$lan" trusted "$lan" || return 1; } - for v in "${@:3}"; do set_zone "demo-$v" trusted "$v" || return 1; done; } -hqr_vlans(){ echo "$HQ_RTR_LAN_IF.$VLAN_SRV $HQ_RTR_LAN_IF.$VLAN_CLI $HQ_RTR_LAN_IF.$VLAN_MGMT"; } -hqr_nat(){ need_iface HQ_RTR_WAN_IF "WAN-интерфейс"; need_iface HQ_RTR_LAN_IF "LAN-интерфейс" - rtr_zones "$HQ_RTR_WAN_IF" "$HQ_RTR_LAN_IF" $(hqr_vlans) || return 1 - fw --zone=external --add-masquerade && fw_reload && enable_forward && ok "NAT через $HQ_RTR_WAN_IF"; } -gre_up(){ local l=$1 r=$2 a=$3; fw_on || return 1 - net_gre "$l" "$r" "$a" || return 1 - set_zone "demo-$GRE_IF" trusted "$GRE_IF"; fw --zone=external --add-protocol=gre && fw_reload && ok "GRE $GRE_IF: $l → $r"; } -hqr_gre(){ gre_up "$HQ_RTR_WAN_IP" "$BR_RTR_WAN_IP" "$HQ_GRE_IP/$GRE_PFX"; } -hqr_ospf(){ frr_ospf "$HQ_GRE_IP" "$GRE_IF" "" "$GRE_NET/$GRE_PFX" "$SRV_NET/$SRV_PFX" "$CLI_NET/$CLI_PFX" "$MGMT_NET/$MGMT_PFX"; } -hqr_dhcp(){ need_iface HQ_RTR_LAN_IF "LAN-интерфейс (транк)"; pkg dnsmasq || return 1 - { echo "# demo-setup: DHCP для VLAN $VLAN_CLI"; echo "port=0"; echo "interface=$HQ_RTR_LAN_IF.$VLAN_CLI"; echo "bind-interfaces" - echo "dhcp-authoritative"; echo "dhcp-range=$DHCP_START,$DHCP_END,$(pfx2mask "$CLI_PFX"),12h" - echo "dhcp-option=option:router,$HQ_RTR_CLI_IP"; echo "dhcp-option=option:dns-server,$HQ_SRV_IP" - echo "dhcp-option=option:domain-name,$DOMAIN"; echo "domain=$DOMAIN" - [[ -n $HQ_CLI_MAC ]] && echo "dhcp-host=$HQ_CLI_MAC,$HQ_CLI_IP"; true; } | put /etc/dnsmasq.conf || return 1 - ((DRY)) || dnsmasq --test || return 1 - svc dnsmasq && ok "DHCP: $DHCP_START–$DHCP_END, шлюз $HQ_RTR_CLI_IP, DNS $HQ_SRV_IP"; } -dnat(){ local p=$1 to=$2 tp=$3; fw --zone=external --add-forward-port="port=$p:proto=tcp:toport=$tp:toaddr=$to" && ok "DNAT tcp/$p → $to:$tp"; } -hqr_dnat(){ fw_on || return 1; dnat 8080 "$HQ_SRV_IP" 80 && dnat "$SSH_PORT" "$HQ_SRV_IP" "$SSH_PORT" && fw_reload; } -hqr_ntp(){ chrony_client "$ISP_HQ_IP"; } -chk_rtr(){ ip -br a; ip r; firewall-cmd --get-active-zones 2>/dev/null; firewall-cmd --zone=external --list-all 2>/dev/null - vtysh -c 'show ip ospf neighbor' 2>/dev/null; ping -c2 -W1 "$DNS_FWD"; } -chk_hqr(){ chk_rtr; ping -c2 -W1 "$BR_GRE_IP"; ping -c2 -W1 "$HQ_SRV_IP"; } -M_hqr=( -"М1|Сеть: WAN и VLAN $VLAN_SRV/$VLAN_CLI/$VLAN_MGMT (router-on-a-stick)|hqr_net" "М1|Пользователь $ADM_USER (sudo без пароля)|u_rtr" -"М1|NAT в сторону ISP|hqr_nat" "М1|GRE-туннель до BR-RTR|hqr_gre" "М1|OSPF по туннелю (MD5)|hqr_ospf" -"М1|DHCP для HQ-CLI|hqr_dhcp" "М1|Часовой пояс|c_tz" -"М2|SSH с паролем (Ansible)|c_sshd_basic" "М2|NTP-клиент → ISP|hqr_ntp" "М2|DNAT 8080 → web, $SSH_PORT → HQ-SRV|hqr_dnat") - -# ================================= BR-RTR ==================================== -brr_net(){ over_ssh_warn - need_iface BR_RTR_WAN_IF "WAN-интерфейс (к ISP)"; need_iface BR_RTR_LAN_IF "Интерфейс к BR-FW" - net_eth "$BR_RTR_WAN_IF" static "$BR_RTR_WAN_IP/$BR_WAN_PFX" "$ISP_BR_IP" "$DNS_FWD" || return 1 - net_eth "$BR_RTR_LAN_IF" static "$BR_RTR_FW_IP/$FW_PFX" || return 1 - enable_forward; } -brr_nat(){ need_iface BR_RTR_WAN_IF "WAN-интерфейс"; need_iface BR_RTR_LAN_IF "Интерфейс к BR-FW" - rtr_zones "$BR_RTR_WAN_IF" "$BR_RTR_LAN_IF" || return 1 - fw --zone=external --add-masquerade && fw_reload && enable_forward && ok "NAT через $BR_RTR_WAN_IF"; } -brr_gre(){ gre_up "$BR_RTR_WAN_IP" "$HQ_RTR_WAN_IP" "$BR_GRE_IP/$GRE_PFX"; } -brr_ospf(){ need_iface BR_RTR_LAN_IF "Интерфейс к BR-FW"; local a=$GRE_IF p=$BR_RTR_LAN_IF - [[ $OSPF_FW_AUTH == yes ]] && { a="$GRE_IF $BR_RTR_LAN_IF"; p=""; } - frr_ospf "$BR_GRE_IP" "$a" "$p" "$GRE_NET/$GRE_PFX" "$FW_NET/$FW_PFX"; } -brr_dnat(){ fw_on || return 1; dnat 8080 "$BR_SRV_IP" 8080 && dnat "$SSH_PORT" "$BR_SRV_IP" "$SSH_PORT" && fw_reload; } -brr_ntp(){ chrony_client "$ISP_BR_IP"; } -chk_brr(){ chk_rtr; ping -c2 -W1 "$HQ_GRE_IP"; ping -c2 -W1 "$BR_FW_IP"; } -M_brr=( -"М1|Сеть: WAN и линк к BR-FW|brr_net" "М1|Пользователь $ADM_USER (sudo без пароля)|u_rtr" "М1|NAT в сторону ISP|brr_nat" -"М1|GRE-туннель до HQ-RTR|brr_gre" "М1|OSPF: туннель (MD5) и линк к BR-FW|brr_ospf" "М1|Часовой пояс|c_tz" -"М2|SSH с паролем (Ansible)|c_sshd_basic" "М2|NTP-клиент → ISP|brr_ntp" "М2|DNAT 8080 → testapp, $SSH_PORT → BR-SRV|brr_dnat") - -# ================================= BR-FW ===================================== -brfw_net(){ over_ssh_warn; warn "Вариант для Linux. Если BR-FW — Ideco NGFW, настройте его в веб-интерфейсе." - need_iface BR_FW_WAN_IF "Интерфейс к BR-RTR"; need_iface BR_FW_LAN_IF "Интерфейс к BR-SRV" - net_eth "$BR_FW_WAN_IF" static "$BR_FW_IP/$FW_PFX" "$BR_RTR_FW_IP" "$HQ_SRV_IP" || return 1 - net_eth "$BR_FW_LAN_IF" static "$BR_FW_LAN_IP/$BR_PFX" || return 1 - enable_forward || return 1 - if ((DRY)) || systemctl is-active --quiet firewalld; then - set_zone "demo-$BR_FW_WAN_IF" trusted "$BR_FW_WAN_IF"; set_zone "demo-$BR_FW_LAN_IF" trusted "$BR_FW_LAN_IF" - warn "Оба интерфейса в зоне trusted. Ограничения межсетевого экрана задайте по заданию."; fi; } -brfw_ospf(){ need_iface BR_FW_WAN_IF "Интерфейс к BR-RTR"; local a="" p=$BR_FW_WAN_IF - [[ $OSPF_FW_AUTH == yes ]] && { a=$BR_FW_WAN_IF; p=""; } - frr_ospf "$BR_FW_IP" "$a" "$p" "$FW_NET/$FW_PFX" "$BR_NET/$BR_PFX"; } -chk_brfw(){ ip -br a; ip r; vtysh -c 'show ip ospf neighbor' 2>/dev/null; ping -c2 -W1 "$BR_RTR_FW_IP"; ping -c2 -W1 "$BR_SRV_IP"; } -M_brfw=("М1|Сеть и маршрутизация|brfw_net" "М1|OSPF на интерфейсе к BR-RTR|brfw_ospf" "М1|Часовой пояс|c_tz") - -# ================================= HQ-SW ===================================== -hqsw_ovs(){ over_ssh_warn - pkg_any "openvswitch" "openvswitch3" || return 1; svc openvswitch openvswitch-switch || return 1 - need_iface HQ_SW_UP_IF "Аплинк к HQ-RTR (транк)"; need_iface HQ_SW_SRV_IF "Порт к HQ-SRV"; need_iface HQ_SW_CLI_IF "Порт к HQ-CLI" - local i um="interface-name:hq-sw;interface-name:mgmt;interface-name:ovs-system" - for i in "$HQ_SW_UP_IF" "$HQ_SW_SRV_IF" "$HQ_SW_CLI_IF"; do um+=";interface-name:$i"; done - put /etc/NetworkManager/conf.d/90-demo-ovs.conf <<< $'[keyfile]\n'"unmanaged-devices=$um" || return 1 - run systemctl reload NetworkManager || return 1 - run ovs-vsctl --may-exist add-br hq-sw || return 1 - run ovs-vsctl --may-exist add-port hq-sw "$HQ_SW_UP_IF" -- set port "$HQ_SW_UP_IF" trunks="$VLAN_SRV,$VLAN_CLI,$VLAN_MGMT" || return 1 - run ovs-vsctl --may-exist add-port hq-sw "$HQ_SW_SRV_IF" -- set port "$HQ_SW_SRV_IF" tag="$VLAN_SRV" || return 1 - run ovs-vsctl --may-exist add-port hq-sw "$HQ_SW_CLI_IF" -- set port "$HQ_SW_CLI_IF" tag="$VLAN_CLI" || return 1 - run ovs-vsctl --may-exist add-port hq-sw mgmt -- set port mgmt tag="$VLAN_MGMT" -- set interface mgmt type=internal || return 1 - put /etc/systemd/system/demo-hqsw-mgmt.service </dev/null | head -1) - while read -r d; do [[ $d == "$root" ]] && continue - [[ $(lsblk -no NAME "/dev/$d" | wc -l) -eq 1 && -z $(lsblk -no MOUNTPOINT "/dev/$d" | tr -d ' ') && -z $(blkid -p "/dev/$d" 2>/dev/null) ]] && cand+=("$d") - done < <(lsblk -dno NAME,TYPE | awk '$2=="disk"{print $1}') - ((${#cand[@]} >= 2)) || ((DRY)) || { err "Нужно минимум 2 пустых диска без разделов и файловых систем"; lsblk; return 1; } - ((DRY)) && cand=(sdb sdc) - lsblk -o NAME,SIZE,TYPE,MOUNTPOINT; echo "Пустые диски: ${cand[*]}" - d=$(ui_input "RAID$RAID_LEVEL" "Пустые диски: ${cand[*]}"$'\n'"Введите диски для RAID через пробел:" "${cand[*]}") || return 1; read -ra pick <<< "$d" - ((${#pick[@]} >= 2)) || { err "Нужно минимум 2 диска"; return 1; } - for d in "${pick[@]}"; do [[ " ${cand[*]} " == *" $d "* ]] || { err "$d не в списке пустых дисков"; return 1; }; done - warn "ВСЕ ДАННЫЕ на ${pick[*]} будут уничтожены." - d=$(ui_input "Подтверждение" "ВСЕ ДАННЫЕ на ${pick[*]} будут уничтожены. Введите УДАЛИТЬ:" "") || return 1; [[ $d == УДАЛИТЬ ]] || { warn "Отменено"; return 1; } - run mdadm --create /dev/md0 --run --level="$RAID_LEVEL" --raid-devices="${#pick[@]}" "${pick[@]/#//dev/}" || return 1 - run mkfs.ext4 -F /dev/md0 || return 1 - ((DRY)) || mdadm --detail --scan | grep md0 | put /etc/mdadm.conf - run mkdir -p "$RAID_MOUNT" || return 1 - if ((DRY)); then echo "${C_C}+ /etc/fstab: UUID=... $RAID_MOUNT ext4 defaults 0 0${C_0}" - else backup /etc/fstab; echo "UUID=$(blkid -s UUID -o value /dev/md0) $RAID_MOUNT ext4 defaults 0 0" >> /etc/fstab; fi - run systemctl daemon-reload; run mount "$RAID_MOUNT" && ok "RAID$RAID_LEVEL /dev/md0 смонтирован в $RAID_MOUNT"; ((DRY)) || df -h "$RAID_MOUNT"; } -hqs_nfs(){ pkg nfs-utils || return 1 - ((DRY)) || mountpoint -q "$RAID_MOUNT" || { err "$RAID_MOUNT не смонтирован — сначала RAID"; return 1; } - run mkdir -p "$RAID_MOUNT/nfs" && run chmod 1777 "$RAID_MOUNT/nfs" || return 1 - put /etc/exports.d/demo.exports <<< "$RAID_MOUNT/nfs $CLI_NET/$CLI_PFX(rw,sync,no_subtree_check,root_squash)" || return 1 - svc nfs-server || return 1; run exportfs -ra || return 1; fw_open nfs mountd rpc-bind - ((DRY)) || exportfs -v; ok "NFS: $RAID_MOUNT/nfs для $CLI_NET/$CLI_PFX"; } -hqs_ntp(){ chrony_client "$ISP_HQ_IP"; } -hqs_web(){ need_pass || return 1; local src sql root=/var/www/html - src=$(iso_find 'index.php'); [[ -n $src ]] || { err "index.php не найден в ISO ($ISO_DIR)"; return 1; }; src=${src%/*} - msg "Файлы веб-приложения: $src"; ((DRY)) || ls -l "$src" - pkg_any "httpd php php-mysqlnd mariadb-server mariadb" "httpd php php-mysqlnd mariadb-server" || return 1 - svc mariadb || return 1 - if ((DRY)); then echo "${C_C}+ mysql: CREATE DATABASE webdb; пользователь web@localhost${C_0}" - else mysql -u root </dev/null; rm -f "$root/index.html" - sed -i -E "s/(\\\$servername[[:space:]]*=[[:space:]]*)[\"'][^\"']*[\"']/\1\"localhost\"/; s/(\\\$username[[:space:]]*=[[:space:]]*)[\"'][^\"']*[\"']/\1\"web\"/; s/(\\\$password[[:space:]]*=[[:space:]]*)[\"'][^\"']*[\"']/\1\"$PASS\"/; s/(\\\$dbname[[:space:]]*=[[:space:]]*)[\"'][^\"']*[\"']/\1\"webdb\"/" "$root/index.php" - chown -R apache:apache "$root"; selinux_on && restorecon -R "$root"; fi - svc httpd || return 1; fw_open http - ((DRY)) || curl -s -o /dev/null -w "web: HTTP %{http_code}\n" http://localhost/ - warn "Сверьте параметры подключения в $root/index.php с заданием."; } -hqs_ca(){ local D=/etc/pki/demo-ca n H K CK MD=-sha256; pkg openssl || return 1 - K="-newkey rsa:2048"; CK="-newkey rsa:4096" - if [[ $CERT_ALG == gost ]]; then pkg openssl-gost-engine || { err "ГОСТ-движок не установлен — выберите rsa"; return 1; } - K="-newkey gost2012_256 -pkeyopt paramset:A"; CK=$K; MD=-md_gost12_256; warn "ГОСТ-TLS должны поддерживать nginx и браузер."; fi - id "$SSH_USER" >/dev/null 2>&1 || ((DRY)) || { err "Нет пользователя $SSH_USER — выполните пункт «Пользователь»"; return 1; } - run mkdir -p "$D" && run chmod 700 "$D" || return 1 - run openssl req -x509 $CK -nodes $MD -days 3650 -keyout "$D/ca.key" -out "$D/ca.crt" -subj "/CN=AU-TEAM Root CA/O=AU-TEAM" \ - -addext "basicConstraints=critical,CA:TRUE" -addext "keyUsage=critical,keyCertSign,cRLSign" || return 1 - for n in web docker; do - run openssl req $K -nodes $MD -keyout "$D/$n.key" -out "$D/$n.csr" -subj "/CN=$(fqdn $n)" || return 1 - printf 'subjectAltName=DNS:%s\nbasicConstraints=CA:FALSE\nkeyUsage=digitalSignature,keyEncipherment\nextendedKeyUsage=serverAuth\n' "$(fqdn $n)" | put "$D/$n.ext" || return 1 - run openssl x509 -req -in "$D/$n.csr" -CA "$D/ca.crt" -CAkey "$D/ca.key" -CAcreateserial -days 30 $MD -extfile "$D/$n.ext" -out "$D/$n.crt" || return 1 - done - H=$(getent passwd "$SSH_USER" | cut -d: -f6); H=${H:-/home/$SSH_USER} - run install -d -m 700 -o "$SSH_USER" "$H/certs" || return 1 - run install -m 600 -o "$SSH_USER" "$D/ca.crt" "$D/web.crt" "$D/web.key" "$D/docker.crt" "$D/docker.key" "$H/certs/" || return 1 - ok "CA: $D; сертификаты web/docker (30 дней) — в $H/certs" - if [[ -d $RAID_MOUNT/nfs ]]; then run install -m 644 "$D/ca.crt" "$RAID_MOUNT/nfs/ca.crt" && ok "ca.crt скопирован в $RAID_MOUNT/nfs" - else warn "$RAID_MOUNT/nfs нет — сначала выполните RAID+NFS, затем повторите CA (или скопируйте ca.crt вручную)"; fi; } -hqs_atop(){ pkg atop || return 1; set_kv /etc/sysconfig/atop LOGINTERVAL 420 || return 1; svc atop && ok "atop: интервал 420 с (7 минут)" - warn "Проверьте, что atop.service использует LOGINTERVAL: systemctl cat atop"; } -hqs_cups(){ pkg_any "cups cups-pdf" || return 1; svc cups || return 1 - run cupsctl --share-printers --remote-any || return 1 - run lpadmin -p PDF -E -v cups-pdf:/ -m "$(lpinfo -m 2>/dev/null | grep -i pdf | head -1 | cut -d' ' -f1)" || run lpadmin -p PDF -E -v cups-pdf:/ -m raw || return 1 - run lpadmin -p PDF -o printer-is-shared=true && run lpadmin -d PDF && run cupsenable PDF && run cupsaccept PDF || return 1 - run systemctl restart cups; fw_open ipp; ok "Принтер PDF: ipp://$(fqdn hq-srv):631/printers/PDF"; } -hqs_f2b(){ pkg_any "fail2ban fail2ban-firewalld" "fail2ban" || return 1 - put /etc/fail2ban/jail.d/demo-sshd.local </dev/null - for n in hq-rtr hq-srv br-srv web docker; do getent hosts "$(fqdn $n)"; done; cat /proc/mdstat 2>/dev/null; exportfs -v 2>/dev/null; } -M_hqs=( -"М1|Сеть|hqs_net" "М1|Пользователь $SSH_USER (uid $SSH_UID, sudo без пароля)|u_srv" "М1|SSH: порт $SSH_PORT, только $SSH_USER, 2 попытки, баннер|c_sshd" -"М1|DNS-сервер (dnsmasq: A/PTR, форвардер)|hqs_dns" "М1|Часовой пояс|c_tz" -"М2|DNS: пересылка зон AD на BR-SRV|hqs_adfwd" "М2|RAID$RAID_LEVEL → $RAID_MOUNT (уничтожает данные дисков)|hqs_raid" "М2|NFS $RAID_MOUNT/nfs|hqs_nfs" -"М2|NTP-клиент → ISP|hqs_ntp" "М2|Веб-приложение Apache + MariaDB|hqs_web" -"М3|Центр сертификации и сертификаты web/docker|hqs_ca" "М3|atop (7 минут)|hqs_atop" "М3|CUPS и виртуальный PDF-принтер|hqs_cups" -"М3|fail2ban для SSH|hqs_f2b" "М3|Кибер Бэкап (подсказки)|hqs_backup") - -# ================================= BR-SRV ==================================== -brs_net(){ over_ssh_warn; need_iface BR_SRV_IF "Интерфейс BR-SRV" - net_eth "$BR_SRV_IF" static "$BR_SRV_IP/$BR_PFX" "$BR_FW_LAN_IP" "$HQ_SRV_IP"; } -brs_dc(){ need_pass || return 1 - [[ -f /var/lib/samba/private/sam.ldb ]] && { err "Домен уже развёрнут (sam.ldb существует) — повторное развёртывание отключено"; return 1; } - pkg_any "samba-dc samba-client krb5-workstation" "samba samba-dc krb5-workstation" || return 1 - warn "Будет создан домен $(REALM) (NetBIOS $AD_WORKGROUP); smb.conf и krb5.conf будут заменены (копии .demo-orig)." - yesno "Развернуть контроллер домена?" n || return 1 - local s; for s in smb nmb winbind; do run systemctl disable --now "$s" 2>/dev/null; done - [[ -f /etc/samba/smb.conf ]] && { backup /etc/samba/smb.conf; run rm -f /etc/samba/smb.conf; } - run samba-tool domain provision --use-rfc2307 --realm="$(REALM)" --domain="$AD_WORKGROUP" --server-role=dc \ - --dns-backend=SAMBA_INTERNAL --adminpass="$PASS" --option="dns forwarder = $HQ_SRV_IP" || { err "Развёртывание не удалось"; return 1; } - backup /etc/krb5.conf; run cp -f /var/lib/samba/private/krb5.conf /etc/krb5.conf || return 1 - svc samba || return 1 - if ((DRY)) || firewall-cmd --get-services 2>/dev/null | grep -qw samba-dc; then fw_open samba-dc - else fw_open dns kerberos ldap ldaps 135/tcp 445/tcp 464/tcp 464/udp 3268/tcp 3269/tcp; fi - selinux_on && warn "SELinux в режиме enforcing: при ошибках Samba смотрите журнал (ausearch -m avc)." - ((DRY)) || { sleep 3; samba-tool domain level show; }; ok "Контроллер домена $(REALM) развёрнут"; } -brs_users(){ need_pass || return 1; local i - run samba-tool domain passwordsettings set --min-pwd-length=8 --account-lockout-threshold=5 --account-lockout-duration=1 --reset-account-lockout-after=1 || return 1 - run samba-tool group add hq 2>/dev/null - for i in 1 2 3 4 5; do run samba-tool user add "hquser$i" "$PASS" 2>/dev/null || warn "hquser$i не создан (возможно, уже есть)"; done - run samba-tool group addmembers hq hquser1,hquser2,hquser3,hquser4,hquser5 - ((DRY)) || samba-tool group listmembers hq; ok "Пользователи hquser1–5 в группе hq"; } -brs_import(){ need_pass || return 1; local c lc fc pc gc sep=';' line u p - c=$(iso_find 'users*.csv'); [[ -n $c ]] || { err "users.csv не найден в ISO"; return 1; }; msg "CSV: $c"; ((DRY)) && return 0 - head -3 "$c" | ui_text "Начало $c" - lc=$(ui_input "Импорт" "Номер колонки логина:" "1") || return 1; fc=$(ui_input "Импорт" "Колонка имени (пусто = нет):" "") || return 1 - pc=$(ui_input "Импорт" "Колонка пароля (пусто = общий пароль):" "") || return 1; gc=$(ui_input "Импорт" "Колонка группы (пусто = нет):" "") || return 1 - for x in "$lc" "$fc" "$pc" "$gc"; do [[ -z $x || $x =~ ^[1-9][0-9]?$ ]] || { err "Номер колонки: $x"; return 1; }; done - [[ -n $lc ]] || return 1; head -1 "$c" | grep -q ';' || sep=',' - tail -n +2 "$c" | tr -d '\r' | while IFS= read -r line; do [[ -z $line ]] && continue - IFS="$sep" read -ra F <<< "$line"; u=${F[lc-1]}; p=${pc:+${F[pc-1]}}; p=${p:-$PASS} - is_name "$u" || { warn "Пропуск строки: логин «$u»"; continue; } - if samba-tool user add "$u" "$p" ${fc:+--given-name="${F[fc-1]}"} >/dev/null 2>&1; then echo "+ $u"; else echo "= $u (уже есть или ошибка)"; fi - [[ -n $gc && -n ${F[gc-1]} ]] && { samba-tool group add "${F[gc-1]}" >/dev/null 2>&1; samba-tool group addmembers "${F[gc-1]}" "$u" >/dev/null 2>&1; } - done; ok "Импорт завершён"; } -brs_ntp(){ chrony_client "$ISP_BR_IP"; } -brs_ansible(){ need_pass || return 1; pkg_any "ansible sshpass" "ansible-core sshpass" || return 1 - put /etc/ansible/hosts 600 <.yml - copy: - dest: "/etc/ansible/PC-INFO/{{ ansible_hostname }}.yml" - content: | - hostname: {{ ansible_hostname }} - ip_address: {{ ansible_default_ipv4.address }} - delegate_to: localhost - become: no -EOF - ((DRY)) || { ansible-playbook /etc/ansible/inventory.yml; cat /etc/ansible/PC-INFO/*.yml 2>/dev/null; } - warn "Если в задании другие хосты или имя плейбука — исправьте /etc/ansible/inventory.yml."; } -brs_docker(){ need_pass || return 1; local d f site db - pkg_any "docker-ce docker-ce-cli docker-compose-plugin" "docker-ce docker-compose" "docker docker-compose" || return 1; svc docker || return 1 - d=$(iso_find 'site_latest*'); [[ -n $d ]] || { err "Образ site_latest не найден в ISO"; return 1; }; d=${d%/*} - for f in "$d"/*.tar "$d"/*.tar.gz; do [[ -f $f ]] && run docker load -i "$f"; done - f=$(find "$d" -maxdepth 1 -iname '*readme*' 2>/dev/null | head -1); [[ -n $f ]] && { warn "README из ISO — сверьте переменные:"; cat "$f"; } - site=$(docker images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -i site | head -1) - db=$(docker images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -i mariadb | head -1) - run install -d -m 700 /root/testapp || return 1 - put /root/testapp/compose.yaml 600 </dev/null 2>&1 || ((DRY)); then run docker compose -f /root/testapp/compose.yaml up -d || return 1 - else run docker-compose -f /root/testapp/compose.yaml up -d || return 1; fi - fw_open 8080/tcp; ((DRY)) || { sleep 5; docker ps; curl -s -o /dev/null -w "testapp: HTTP %{http_code}\n" http://localhost:8080/; }; } -chk_brs(){ ip -br a; ip r; systemctl is-active samba sshd chronyd docker 2>/dev/null; samba-tool user list 2>/dev/null | head - host -t SRV "_ldap._tcp.$DOMAIN" 127.0.0.1 2>/dev/null; docker ps 2>/dev/null; } -M_brs=( -"М1|Сеть|brs_net" "М1|Пользователь $SSH_USER (uid $SSH_UID, sudo без пароля)|u_srv" "М1|SSH: порт $SSH_PORT, только $SSH_USER, 2 попытки, баннер|c_sshd" "М1|Часовой пояс|c_tz" -"М2|Контроллер домена Samba AD|brs_dc" "М2|Пользователи hquser1–5, группа hq, политика паролей|brs_users" "М2|NTP-клиент → ISP|brs_ntp" -"М2|Ansible: инвентарь и проверка ping|brs_ansible" "М2|Docker: testapp и MariaDB из ISO|brs_docker" -"М3|Импорт пользователей из users.csv|brs_import" "М3|Ansible-плейбук PC-INFO|brs_pcinfo") - -# ================================= HQ-CLI ==================================== -hqc_net(){ over_ssh_warn; need_iface HQ_CLI_IF "Интерфейс HQ-CLI"; net_eth "$HQ_CLI_IF" dhcp || return 1; ((DRY)) || { ip -br a show "$HQ_CLI_IF"; cat /etc/resolv.conf; }; } -hqc_join(){ need_pass || return 1 - pkg_any "realmd sssd sssd-tools oddjob oddjob-mkhomedir adcli samba-common-tools krb5-workstation authselect" "realmd sssd adcli oddjob-mkhomedir krb5-workstation authselect" || return 1 - ((DRY)) || { getent hosts "$(fqdn br-srv)" >/dev/null || warn "br-srv не резолвится: проверьте DNS"; realm discover "$DOMAIN" || warn "Домен не обнаружен: проверьте пересылку зон AD на HQ-SRV"; } - if ((DRY)); then run realm join -U "$AD_ADMIN" "$DOMAIN" - else printf '%s\n' "$PASS" | realm join -v -U "$AD_ADMIN" "$DOMAIN" || { err "Ввод в домен не удался (DNS, время, пароль)"; return 1; }; fi - if ((DRY)); then echo "${C_C}+ sssd.conf: use_fully_qualified_names = False, fallback_homedir = /home/%u${C_0}" - else backup /etc/sssd/sssd.conf; sed -i -E 's/^use_fully_qualified_names.*/use_fully_qualified_names = False/; s|^fallback_homedir.*|fallback_homedir = /home/%u|' /etc/sssd/sssd.conf; fi - run authselect select sssd with-mkhomedir --force || return 1; svc oddjobd; svc sssd || return 1 - ((DRY)) || { realm list; id hquser1; }; ok "HQ-CLI в домене $DOMAIN; перезагрузите ВМ"; } -hqc_sudo(){ pkg sudo || return 1; local f=/etc/sudoers.d/demo-hq - put "$f" 440 <<< "%hq ALL=(ALL) /usr/bin/cat, /usr/bin/grep, /usr/bin/id" || return 1 - ((DRY)) || visudo -cf "$f" >/dev/null || { err "Ошибка sudoers — файл удалён"; rm -f "$f"; return 1; }; ok "Группа hq: sudo только cat, grep, id"; } -hqc_nfs(){ pkg nfs-utils || return 1; run mkdir -p /mnt/nfs || return 1 - if ((DRY)); then echo "${C_C}+ /etc/fstab: $HQ_SRV_IP:$RAID_MOUNT/nfs /mnt/nfs nfs defaults,_netdev,x-systemd.automount 0 0${C_0}" - else backup /etc/fstab; sed -i '\| /mnt/nfs |d' /etc/fstab; echo "$HQ_SRV_IP:$RAID_MOUNT/nfs /mnt/nfs nfs defaults,_netdev,x-systemd.automount 0 0" >> /etc/fstab; fi - run systemctl daemon-reload; run mount /mnt/nfs && ok "NFS смонтирован в /mnt/nfs"; ((DRY)) || df -h /mnt/nfs; } -hqc_ntp(){ chrony_client "$ISP_HQ_IP"; } -hqc_yandex(){ pkg_any "yandex-browser-stable" "yandex-browser" || warn "Пакет не найден — установите Яндекс Браузер вручную"; } -hqc_ssh(){ id "$CLI_SSH_USER" >/dev/null 2>&1 || mkuser "$CLI_SSH_USER" || return 1; c_sshd_basic "$CLI_SSH_PORT"; } -hqc_ca(){ local ca=/mnt/nfs/ca.crt h - [[ -f $ca ]] || ((DRY)) || ca=$(ui_input "Сертификат CA" "Путь к ca.crt:" "$ca"); [[ -f $ca ]] || ((DRY)) || { err "ca.crt не найден"; return 1; } - run install -m 644 "$ca" /etc/pki/ca-trust/source/anchors/au-team-ca.crt && run update-ca-trust || return 1 - pkg nss-tools || return 1 - for h in /etc/skel /root /home/*; do [[ -d $h ]] || continue - run mkdir -p "$h/.pki/nssdb"; [[ -f $h/.pki/nssdb/cert9.db ]] || run certutil -d "sql:$h/.pki/nssdb" -N --empty-password - run certutil -d "sql:$h/.pki/nssdb" -A -t "C,," -n "AU-TEAM CA" -i "$ca" - [[ $h == /home/* ]] && run chown -R "$(stat -c %U "$h")": "$h/.pki"; done - ok "CA добавлен в системное хранилище и NSS браузеров"; } -hqc_printer(){ pkg_any "cups cups-client" "cups" || return 1; svc cups || return 1 - run lpadmin -p PDF -E -v "ipp://$(fqdn hq-srv):631/printers/PDF" -m everywhere || return 1 - run lpadmin -d PDF && ok "Принтер PDF по умолчанию"; ((DRY)) || lpstat -d; } -hqc_backup(){ run install -d -m 700 /backup && ok "Создан /backup" - warn "Установите агент и узел хранения Кибер Бэкап, зарегистрируйте их на сервере управления; хранилище — /backup."; } -chk_hqc(){ ip -br a; ip r; cat /etc/resolv.conf; realm list 2>/dev/null; id hquser1 2>/dev/null; df -h /mnt/nfs 2>/dev/null; lpstat -d 2>/dev/null; } -M_hqc=( -"М1|Сеть: DHCP|hqc_net" "М1|Часовой пояс|c_tz" "М2|Ввод в домен $(REALM)|hqc_join" "М2|sudo для группы hq (cat, grep, id)|hqc_sudo" -"М2|NFS: автомонтирование /mnt/nfs|hqc_nfs" "М2|NTP-клиент → ISP|hqc_ntp" "М2|Яндекс Браузер|hqc_yandex" "М2|SSH для Ansible|hqc_ssh" -"М3|Доверие к CA (HTTPS без ошибок)|hqc_ca" "М3|Принтер PDF по умолчанию|hqc_printer" "М3|Кибер Бэкап: каталог /backup|hqc_backup") - - -# ============================ интерфейс (окна) =============================== -# Окна whiptail/dialog, если они есть; иначе текстовое меню. DEMO_UI=text — всегда текст. -UI=text; UI_TTY=${DEMO_TTY:-/dev/tty} -ui_init(){ local b - case ${DEMO_UI:-auto} in - text) UI=text; return ;; - whiptail|dialog) UI=$DEMO_UI; return ;; - esac - [[ -t 0 && -t 1 || -t 0 && $MODE == apply ]] || return 0 - for b in whiptail dialog; do have "$b" && { UI=$b; return; }; done - if [[ $MODE == apply ]] && yesno "Окна выбора недоступны. Установить пакет newt (whiptail)?" y; then - dnf install -y newt >/dev/null 2>&1 && have whiptail && UI=whiptail; fi; return 0; } -ui_size(){ local l c; l=$(tput lines 2>/dev/null || echo 24); c=$(tput cols 2>/dev/null || echo 80) - UI_H=$(( l > 10 ? l - 4 : 20 )); UI_W=$(( c > 90 ? 86 : (c > 40 ? c - 4 : 76) )); } -# ui_call АРГУМЕНТЫ — запускает whiptail/dialog, результат печатает в stdout. -ui_call(){ local rc out; ui_size - out=$("$UI" --backtitle "demo-setup v$VERSION — $([[ $MODE == apply ]] && echo ПРИМЕНЕНИЕ || echo ПРОСМОТР)" "$@" 3>&1 1>"$UI_TTY" 2>&3 <&5); rc=$? - printf '%s' "$out"; return $rc; } -btn(){ if [[ $UI == dialog ]]; then echo "--yes-label $1 --no-label $2 --ok-label $1 --cancel-label $2"; else echo "--yes-button $1 --no-button $2 --ok-button $1 --cancel-button $2"; fi; } -# ui_menu "заголовок" "текст" ТЕГ ПУНКТ ТЕГ ПУНКТ ... → выбранный тег -ui_menu(){ local t=$1 x=$2; shift 2; local n=$(( $# / 2 )) a i - if [[ $UI != text ]]; then ui_size; local h=$(( n + 8 < UI_H ? n + 8 : UI_H )) - ui_call --title "$t" $(btn Выбрать Назад) --menu "$x" "$h" "$UI_W" $(( h - 7 )) "$@"; return; fi - local -a args=("$@") - { echo; echo "${C_B}===== $t =====${C_0}"; [[ -n $x ]] && echo "$x" - for ((i = 0; i < ${#args[@]}; i += 2)); do printf ' %4s) %s\n' "${args[i]}" "${args[i+1]}"; done; } >&2 - read -r -p "Выбор: " a || return 255 - for ((i = 0; i < ${#args[@]}; i += 2)); do [[ ${args[i],,} == "${a,,}" ]] && { printf '%s' "${args[i]}"; return 0; }; done - [[ -z $a ]] && return 1; warn "Нет такого пункта: $a" >&2; ui_menu "$t" "$x" "$@"; } -# ui_input "заголовок" "вопрос" "значение по умолчанию" → введённая строка -ui_input(){ local a - if [[ $UI != text ]]; then ui_size; ui_call --title "$1" $(btn OK Отмена) --inputbox "$2" 12 "$UI_W" "$3"; return; fi - read -r -p "$2 [$3]: " a || return 1; printf '%s' "${a:-$3}"; } -ui_pass(){ local a - if [[ $UI != text ]]; then ui_call --title "Пароль" $(btn OK Отмена) --passwordbox "$1" 10 "$UI_W"; return; fi - read -r -s -p "$1: " a || return 1; echo >&2; printf '%s' "$a"; } -ui_yesno(){ local q=$1 d=${2:-n} a h - if [[ $UI != text ]]; then ui_size; local -a dn=(); [[ $d == n ]] && dn=(--defaultno) - ui_call --title "Подтверждение" $(btn Да Нет) "${dn[@]}" --yesno "$q" 12 "$UI_W" >/dev/null; return; fi - [[ $d == y ]] && h="Y/n" || h="y/N"; read -r -p "$q [$h] " a || return 1; a=${a:-$d}; [[ $a =~ ^[YyДд] ]]; } -# ui_text "заголовок" — показывает текст из stdin (с прокруткой). -ui_text(){ local f; f=$(mktemp) || return 1; cat > "$f" - if [[ $UI != text ]]; then ui_size; ui_call --title "$1" --scrolltext --textbox "$f" "$UI_H" "$UI_W" >/dev/null 2>&1 || ui_call --title "$1" --textbox "$f" "$UI_H" "$UI_W" >/dev/null - else echo; echo "${C_B}===== $1 =====${C_0}"; cat "$f"; pause; fi; rm -f "$f"; } -ui_clear(){ [[ $UI != text ]] && clear 2>/dev/null; return 0; } - -# ========================== ввод данных пользователем ======================== -declare -A IF_LABEL=( -[ISP_WAN_IF]="ISP: интерфейс в Интернет" [ISP_HQ_IF]="ISP: интерфейс к HQ-RTR" [ISP_BR_IF]="ISP: интерфейс к BR-RTR" -[HQ_RTR_WAN_IF]="HQ-RTR: WAN-интерфейс" [HQ_RTR_LAN_IF]="HQ-RTR: LAN (транк к HQ-SW)" [BR_RTR_WAN_IF]="BR-RTR: WAN-интерфейс" -[BR_RTR_LAN_IF]="BR-RTR: интерфейс к BR-FW" [BR_FW_WAN_IF]="BR-FW: интерфейс к BR-RTR" [BR_FW_LAN_IF]="BR-FW: интерфейс к BR-SRV" -[HQ_SW_UP_IF]="HQ-SW: аплинк к HQ-RTR" [HQ_SW_SRV_IF]="HQ-SW: порт к HQ-SRV" [HQ_SW_CLI_IF]="HQ-SW: порт к HQ-CLI" -[HQ_SRV_IF]="HQ-SRV: интерфейс" [BR_SRV_IF]="BR-SRV: интерфейс" [HQ_CLI_IF]="HQ-CLI: интерфейс") -label_of(){ local e; [[ -n ${IF_LABEL[$1]+x} ]] && { echo "${IF_LABEL[$1]}"; return; } - for e in "${PARAMS[@]}"; do [[ ${e%%|*} == "$1" ]] && { echo "${e##*|}"; return; }; done; echo "$1"; } -type_hint(){ case $1 in - ip) echo "IPv4-адрес, например 192.168.1.10" ;; ipo) echo "IPv4-адрес или пусто" ;; - pfx) echo "Префикс 0–32 (можно /28 или маску 255.255.255.240)" ;; port) echo "Порт 1–65535" ;; int) echo "Целое число" ;; - name) echo "Латиница, цифры, точка, дефис" ;; yn) echo "yes или no" ;; alg) echo "rsa или gost" ;; - list) echo "Записи имя=IP или имя=IP:ptr через пробел" ;; *) echo "Текст без кавычек" ;; esac; } -# Приводит «/28» и «255.255.255.240» к префиксу 28. -norm_pfx(){ local v=${1#/}; if is_pfx "$v"; then echo "$v"; elif is_ip "$v"; then mask2pfx "$v"; else return 1; fi; } -# Выбор интерфейса из списка интерфейсов ВМ. -pick_iface(){ local v=$1 i st mac items=() c - for i in /sys/class/net/*; do i=${i##*/}; [[ $i == lo || $i == ovs-system ]] && continue - st=$(cat "/sys/class/net/$i/operstate" 2>/dev/null); mac=$(cat "/sys/class/net/$i/address" 2>/dev/null) - items+=("$i" "$st $mac $(ip -4 -o addr show dev "$i" 2>/dev/null | awk '{print $4}' | head -1)"); done - items+=("*" "Ввести имя вручную") - c=$(ui_menu "$(label_of "$v")" "Текущее значение: ${!v:-не выбрано}. Выберите интерфейс этой ВМ:" "${items[@]}") || return 1 - if [[ $c == "*" ]]; then c=$(ui_input "$(label_of "$v")" "Имя интерфейса (например ens19):" "${!v}") || return 1; fi - if is_iface "$c" || { ((DRY)) && [[ $c =~ ^[A-Za-z0-9._@-]{1,15}$ ]]; }; then printf -v "$v" '%s' "$c"; conf_save; return 0; fi - ui_msg "Интерфейс «$c» не найден на этой ВМ."; return 1; } -ui_msg(){ if [[ $UI != text ]]; then ui_call --title "Сообщение" --msgbox "$1" 12 "${UI_W:-76}" >/dev/null; else warn "$1"; fi; } -# edit_param ПЕРЕМЕННАЯ ["вопрос"] — ввод значения с проверкой. -edit_param(){ local v=$1 q=${2:-} t a; t=$(param_type "$v"); t=${t:-text} - [[ $t == if ]] && { pick_iface "$v"; return; } - [[ -n $q ]] || q=$(label_of "$v") - while :; do a=$(ui_input "$(label_of "$v")" "$q"$'\n'"$(type_hint "$t")" "${!v}") || return 1 - [[ $t == pfx ]] && { a=$(norm_pfx "$a") || { ui_msg "Неверный префикс или маска."; continue; }; } - if valid "$t" "$a"; then printf -v "$v" '%s' "$a"; conf_save; return 0; fi - ui_msg "Неверное значение. $(type_hint "$t")."; done; } -ask(){ edit_param "$1" "$2"; } -ask_iface(){ pick_iface "$1"; } -need_iface(){ [[ -n ${!1} ]] && { is_iface "${!1}" || ((DRY)); } && return 0; pick_iface "$1"; } -show_val(){ local v=$1 t; t=$(param_type "$v") - if [[ -z ${!v} ]]; then [[ $t == if ]] && echo "(не выбран)" || echo "(пусто)" - elif [[ $t == pfx ]]; then echo "/${!v} ($(pfx2mask "${!v}"))"; else echo "${!v}"; fi; } -settings_menu(){ local items=() e k c i - while :; do items=(); i=1 - for e in "${PARAMS[@]}"; do k=${e%%|*}; items+=("$i" "$(padr "${e##*|}" 40)$(show_val "$k")"); i=$((i+1)); done - for k in "${IFACE_VARS[@]}"; do items+=("$i" "$(padr "${IF_LABEL[$k]}" 40)$(show_val "$k")"); i=$((i+1)); done - items+=("0" "Назад") - c=$(ui_menu "Параметры стенда" "Файл: $CONF (пароль не сохраняется)" "${items[@]}") || return - [[ $c == 0 ]] && return - if ((c <= ${#PARAMS[@]})); then e=${PARAMS[c-1]}; edit_param "${e%%|*}"; else edit_param "${IFACE_VARS[c-1-${#PARAMS[@]}]}"; fi; done; } -need_pass(){ [[ -n $PASS ]] && return 0 - if ((DRY)); then PASS='DryRun.Pass1'; return 0; fi - local p1 p2 - p1=$(ui_pass "Общий пароль стенда (8–64 символа: A-Z a-z 0-9 @ . _ ! + -)") || return 1 - p2=$(ui_pass "Повторите пароль") || return 1 - [[ $p1 == "$p2" && ${#p1} -ge 8 && ${#p1} -le 64 && $p1 =~ ^[A-Za-z0-9@._!+-]+$ ]] || { ui_msg "Пароли не совпадают или недопустимы."; return 1; } - PASS=$p1; } - -# padr ТЕКСТ ШИРИНА — дополняет пробелами по числу символов (корректно для кириллицы). -padr(){ local n=$(( $2 - ${#1} )); ((n < 1)) && n=1; printf '%s%*s' "$1" "$n" ''; } - -# ============================ калькулятор масок ============================== -ip2int(){ local IFS=. a b c d; read -r a b c d <<< "$1"; echo $(( (10#$a << 24) | (10#$b << 16) | (10#$c << 8) | 10#$d )); } -int2ip(){ local n=$1; echo "$(( (n >> 24) & 255 )).$(( (n >> 16) & 255 )).$(( (n >> 8) & 255 )).$(( n & 255 ))"; } -pfx2int(){ (( $1 == 0 )) && { echo 0; return; }; echo $(( (0xFFFFFFFF << (32 - $1)) & 0xFFFFFFFF )); } -mask2pfx(){ is_ip "$1" || return 1; local m p; m=$(ip2int "$1") - for ((p = 0; p <= 32; p++)); do (( $(pfx2int "$p") == m )) && { echo "$p"; return 0; }; done; return 1; } -hosts_of(){ local p=$1; if ((p == 32)); then echo 1; elif ((p == 31)); then echo 2; else echo $(( (1 << (32 - p)) - 2 )); fi; } -# net_info IP ПРЕФИКС — сведения о подсети. -net_info(){ local ip=$1 p=$2 a m net bc first last - a=$(ip2int "$ip"); m=$(pfx2int "$p"); net=$(( a & m )); bc=$(( net | (~m & 0xFFFFFFFF) )) - if ((p >= 31)); then first=$net; last=$bc; else first=$((net + 1)); last=$((bc - 1)); fi - local k v; while IFS='|' read -r k v; do echo "$(padr "$k" 22)$v"; done <= 1 && n <= 4294967294)) || { ui_msg "Нужно число узлов."; return; } - for ((p = 32; p >= 0; p--)); do (( $(hosts_of "$p") >= n )) && break; done - { echo "Для $n узлов подходит минимальный префикс /$p (маска $(pfx2mask "$p"))." - echo "Узлов в такой сети: $(hosts_of "$p"); всего адресов: $(( 1 << (32 - p) ))." - ((p > 0)) && echo "Префикс /$((p - 1)) даст $(hosts_of $((p - 1))) узлов (с запасом)."; } | ui_text "Подбор префикса"; } -calc_split(){ local s n p np i base step - s=$(ui_input "Деление сети" "Сеть для деления, например 192.168.0.0/24:" "192.168.0.0/24") || return - parse_cidr "$s" || { ui_msg "Не удалось разобрать «$s»."; return; } - np=$(ui_input "Деление сети" "Новый префикс подсетей (больше /$CALC_PFX):" "$((CALC_PFX + 2 > 32 ? 32 : CALC_PFX + 2))") || return - np=$(norm_pfx "$np") || { ui_msg "Неверный префикс."; return; } - ((np > CALC_PFX)) || { ui_msg "Новый префикс должен быть больше /$CALC_PFX."; return; } - n=$(( 1 << (np - CALC_PFX) )); ((n <= 256)) || { ui_msg "Получится $n подсетей — слишком много для вывода (максимум 256)."; return; } - base=$(( $(ip2int "$CALC_IP") & $(pfx2int "$CALC_PFX") )); step=$(( 1 << (32 - np) )) - { printf '%-4s %-20s %-17s %-17s %s\n' "№" "Подсеть" "Первый узел" "Последний узел" "Широковещ." - for ((i = 0; i < n; i++)); do p=$(( base + i * step )) - if ((np >= 31)); then printf '%-4s %-20s %-17s %-17s %s\n' $((i + 1)) "$(int2ip $p)/$np" "$(int2ip $p)" "$(int2ip $((p + step - 1)))" "-" - else printf '%-4s %-20s %-17s %-17s %s\n' $((i + 1)) "$(int2ip $p)/$np" "$(int2ip $((p + 1)))" "$(int2ip $((p + step - 2)))" "$(int2ip $((p + step - 1)))"; fi - done; } | ui_text "Деление $(int2ip $base)/$CALC_PFX на /$np"; } -calc_table(){ local p; { printf '%-6s %-17s %-12s %s\n' "Префикс" "Маска" "Адресов" "Узлов" - for ((p = 32; p >= 8; p--)); do printf '/%-5s %-17s %-12s %s\n' "$p" "$(pfx2mask "$p")" "$(( 1 << (32 - p) ))" "$(hosts_of "$p")"; done; } | ui_text "Таблица префиксов"; } -# Проверка стенда: каждый адрес должен входить в свою сеть. -in_net(){ local m; m=$(pfx2int "$3"); (( ($(ip2int "$1") & m) == ($(ip2int "$2") & m) )); } -calc_check(){ local line ip net p name bad=0 - { echo "$(padr Узел 18)$(padr IP 17)$(padr Сеть 21)Итог" - while read -r name ip net p; do - if in_net "${!ip}" "${!net}" "${!p}"; then r=OK; else r="НЕ В СЕТИ"; bad=1; fi - printf '%-18s %-16s %-20s %s\n' "$name" "${!ip}" "$(int2ip $(( $(ip2int "${!net}") & $(pfx2int "${!p}") )))/${!p}" "$r" - done <<'EOT' -ISP-to-HQ ISP_HQ_IP HQ_RTR_WAN_IP HQ_WAN_PFX -HQ-RTR_WAN HQ_RTR_WAN_IP ISP_HQ_IP HQ_WAN_PFX -ISP-to-BR ISP_BR_IP BR_RTR_WAN_IP BR_WAN_PFX -HQ-RTR_VLAN_SRV HQ_RTR_SRV_IP SRV_NET SRV_PFX -HQ-SRV HQ_SRV_IP SRV_NET SRV_PFX -HQ-RTR_VLAN_CLI HQ_RTR_CLI_IP CLI_NET CLI_PFX -DHCP-start DHCP_START CLI_NET CLI_PFX -DHCP-end DHCP_END CLI_NET CLI_PFX -HQ-CLI HQ_CLI_IP CLI_NET CLI_PFX -HQ-RTR_MGMT HQ_RTR_MGMT_IP MGMT_NET MGMT_PFX -HQ-SW_MGMT HQ_SW_MGMT_IP MGMT_NET MGMT_PFX -BR-RTR-to-FW BR_RTR_FW_IP FW_NET FW_PFX -BR-FW-to-RTR BR_FW_IP FW_NET FW_PFX -BR-FW_LAN BR_FW_LAN_IP BR_NET BR_PFX -BR-SRV BR_SRV_IP BR_NET BR_PFX -HQ-RTR_GRE HQ_GRE_IP GRE_NET GRE_PFX -BR-RTR_GRE BR_GRE_IP GRE_NET GRE_PFX -EOT - echo; echo "Сети серверов/клиентов: узлов в /$SRV_PFX — $(hosts_of "$SRV_PFX"), в /$CLI_PFX — $(hosts_of "$CLI_PFX"), в /$MGMT_PFX — $(hosts_of "$MGMT_PFX")." - } | ui_text "Проверка адресов стенда"; } -calc_menu(){ local c - while :; do c=$(ui_menu "Калькулятор масок" "Выберите действие:" \ - 1 "Рассчитать подсеть (IP/префикс или IP маска)" 2 "Подобрать префикс по числу узлов" 3 "Разделить сеть на подсети" \ - 4 "Таблица префиксов /8–/32" 5 "Проверить адреса стенда (входят ли в свои сети)" 0 "Назад") || return - case $c in 1) calc_net ;; 2) calc_hosts ;; 3) calc_split ;; 4) calc_table ;; 5) calc_check ;; 0) return ;; esac; done; } - -# ================================== меню ===================================== -# Данные, которые показываются и вводятся перед выполнением пункта. -declare -A TASK_PARAMS=( -[c_tz]="TZ_NAME" [c_hostname]="DOMAIN" [u_rtr]="ADM_USER" [u_srv]="SSH_USER SSH_UID" [c_sshd]="SSH_USER SSH_PORT BANNER_TEXT" -[isp_net]="ISP_WAN_IF ISP_HQ_IF ISP_BR_IF ISP_HQ_IP HQ_WAN_PFX ISP_BR_IP BR_WAN_PFX" -[isp_nat]="ISP_WAN_IF ISP_HQ_IF ISP_BR_IF" [isp_chrony]="NTP_UPSTREAM ISP_HQ_IP HQ_WAN_PFX ISP_BR_IP BR_WAN_PFX" -[isp_proxy]="DOMAIN HQ_RTR_WAN_IP BR_RTR_WAN_IP" [isp_auth]="DOMAIN HQ_RTR_WAN_IP BR_RTR_WAN_IP" [isp_https]="HQ_RTR_WAN_IP SSH_PORT SSH_USER" -[hqr_net]="HQ_RTR_WAN_IF HQ_RTR_LAN_IF HQ_RTR_WAN_IP HQ_WAN_PFX ISP_HQ_IP DNS_FWD VLAN_SRV HQ_RTR_SRV_IP SRV_PFX VLAN_CLI HQ_RTR_CLI_IP CLI_PFX VLAN_MGMT HQ_RTR_MGMT_IP MGMT_PFX" -[hqr_nat]="HQ_RTR_WAN_IF HQ_RTR_LAN_IF" [hqr_gre]="GRE_IF HQ_RTR_WAN_IP BR_RTR_WAN_IP HQ_GRE_IP GRE_PFX" -[hqr_ospf]="HQ_GRE_IP GRE_NET GRE_PFX SRV_NET SRV_PFX CLI_NET CLI_PFX MGMT_NET MGMT_PFX" -[hqr_dhcp]="HQ_RTR_LAN_IF VLAN_CLI DHCP_START DHCP_END CLI_PFX HQ_RTR_CLI_IP HQ_SRV_IP DOMAIN HQ_CLI_MAC HQ_CLI_IP" -[hqr_ntp]="ISP_HQ_IP" [hqr_dnat]="HQ_SRV_IP SSH_PORT" -[brr_net]="BR_RTR_WAN_IF BR_RTR_LAN_IF BR_RTR_WAN_IP BR_WAN_PFX ISP_BR_IP DNS_FWD BR_RTR_FW_IP FW_PFX" -[brr_nat]="BR_RTR_WAN_IF BR_RTR_LAN_IF" [brr_gre]="GRE_IF BR_RTR_WAN_IP HQ_RTR_WAN_IP BR_GRE_IP GRE_PFX" -[brr_ospf]="BR_RTR_LAN_IF OSPF_FW_AUTH BR_GRE_IP GRE_NET GRE_PFX FW_NET FW_PFX" [brr_ntp]="ISP_BR_IP" [brr_dnat]="BR_SRV_IP SSH_PORT" -[brfw_net]="BR_FW_WAN_IF BR_FW_LAN_IF BR_FW_IP FW_PFX BR_RTR_FW_IP BR_FW_LAN_IP BR_PFX HQ_SRV_IP" -[brfw_ospf]="BR_FW_WAN_IF OSPF_FW_AUTH BR_FW_IP FW_NET FW_PFX BR_NET BR_PFX" -[hqsw_ovs]="HQ_SW_UP_IF HQ_SW_SRV_IF HQ_SW_CLI_IF VLAN_SRV VLAN_CLI VLAN_MGMT HQ_SW_MGMT_IP MGMT_PFX HQ_RTR_MGMT_IP" -[hqs_net]="HQ_SRV_IF HQ_SRV_IP SRV_PFX HQ_RTR_SRV_IP DNS_FWD" -[hqs_dns]="DOMAIN HQ_SRV_IP DNS_FWD DNS_FWD2 DNS_RECORDS WEB_DNS_IP DOCKER_DNS_IP" [hqs_adfwd]="DOMAIN BR_SRV_IP" -[hqs_raid]="RAID_LEVEL RAID_MOUNT" [hqs_nfs]="RAID_MOUNT CLI_NET CLI_PFX" [hqs_ntp]="ISP_HQ_IP" [hqs_web]="ISO_SRC ISO_DIR" -[hqs_ca]="CERT_ALG SSH_USER DOMAIN RAID_MOUNT" [hqs_f2b]="SSH_PORT" [hqs_backup]="ISO_SRC ISO_DIR" -[brs_net]="BR_SRV_IF BR_SRV_IP BR_PFX BR_FW_LAN_IP HQ_SRV_IP" [brs_dc]="DOMAIN AD_WORKGROUP HQ_SRV_IP" [brs_ntp]="ISP_BR_IP" -[brs_ansible]="HQ_SRV_IP SSH_USER SSH_PORT HQ_CLI_IP CLI_SSH_USER CLI_SSH_PORT HQ_GRE_IP BR_RTR_FW_IP ADM_USER" -[brs_docker]="ISO_SRC ISO_DIR APP_INNER_PORT" [brs_import]="ISO_SRC ISO_DIR" -[hqc_net]="HQ_CLI_IF" [hqc_join]="DOMAIN AD_ADMIN" [hqc_nfs]="HQ_SRV_IP RAID_MOUNT" [hqc_ntp]="ISP_HQ_IP" -[hqc_ssh]="CLI_SSH_USER CLI_SSH_PORT" [hqc_printer]="DOMAIN" -) -run_item(){ local desc=$1 fn=$2 rc - ui_clear; hdr "$desc"; "$fn"; rc=$? - if ((rc == 0)); then ok "Готово: $desc"; else err "Не выполнено: $desc (код $rc)"; fi; pause; return $rc; } -# task_screen ФУНКЦИЯ "описание" [skip] — окно пункта: данные + выполнить. -# Код: 0 выполнено, 1 ошибка, 2 назад/пропуск. -task_screen(){ local fn=$1 desc=$2 skip=${3:-} vars c i items v - read -ra vars <<< "${TASK_PARAMS[$fn]:-}" - while :; do items=("R" "▶ Выполнить пункт") - i=1; for v in "${vars[@]}"; do items+=("$i" "$(padr "$(label_of "$v")" 38)$(show_val "$v")"); i=$((i+1)); done - ((${#vars[@]})) && items+=("A" "Ввести все данные по очереди") - items+=("K" "Калькулятор масок"); [[ -n $skip ]] && items+=("S" "Пропустить пункт"); items+=("0" "Назад") - c=$(ui_menu "$desc" "Проверьте данные пункта. Выберите строку, чтобы вписать своё значение." "${items[@]}") || return 2 - case $c in - R|r) run_item "$desc" "$fn"; return ;; - A|a) for v in "${vars[@]}"; do edit_param "$v" || break; done ;; - K|k) calc_menu ;; - S|s|0) return 2 ;; - *) [[ $c =~ ^[0-9]+$ ]] && ((c >= 1 && c <= ${#vars[@]})) && edit_param "${vars[c-1]}" ;; - esac; done; } -# module_menu id "заголовок" М1 — список пунктов модуля. -module_menu(){ local id=$1 title=$2 mod=$3; local -n items="M_$id"; local e m d fn i c tags=() fns=() descs=() - while :; do tags=(); fns=(); descs=(); i=1 - for e in "${items[@]}"; do IFS='|' read -r m d fn <<< "$e"; [[ $m == "$mod" ]] || continue - tags+=("$i" "$d"); fns+=("$fn"); descs+=("$d"); i=$((i+1)); done - ((${#fns[@]})) || { ui_msg "В модуле $mod для $title нет пунктов."; return; } - tags+=("*" "Выполнить весь модуль по порядку" "0" "Назад") - c=$(ui_menu "$title — модуль $mod" "Выберите пункт:" "${tags[@]}") || return - case $c in - 0) return ;; - "*") for ((i = 0; i < ${#fns[@]}; i++)); do task_screen "${fns[i]}" "${descs[i]}" skip; rc=$? - ((rc == 1)) && { ui_yesno "Пункт «${descs[i]}» не выполнен. Продолжить модуль?" n || break; }; done ;; - *) [[ $c =~ ^[0-9]+$ ]] && ((c >= 1 && c <= ${#fns[@]})) && task_screen "${fns[c-1]}" "${descs[c-1]}" ;; - esac; done; } -machine_menu(){ local id=$1 title=$2 host=$3; local -n items="M_$id"; local e m c n1=0 n2=0 n3=0 - MACHINE=$host - for e in "${items[@]}"; do case ${e%%|*} in М1) n1=$((n1+1));; М2) n2=$((n2+1));; М3) n3=$((n3+1));; esac; done - while :; do - local -a it=(); ((n1)) && it+=(1 "Модуль 1 ($n1 пунктов)"); ((n2)) && it+=(2 "Модуль 2 ($n2 пунктов)"); ((n3)) && it+=(3 "Модуль 3 ($n3 пунктов)") - it+=(h "Имя хоста → $(fqdn "$host")" p "Подготовка: установить утилиты" c "Проверка состояния машины" k "Калькулятор масок" 0 "Назад") - c=$(ui_menu "$title" "Машина: $title. Выберите модуль:" "${it[@]}") || return - case $c in - 0) return ;; 1|2|3) module_menu "$id" "$title" "М$c" ;; - h) task_screen c_hostname "Имя хоста $(fqdn "$host")" ;; - p) task_screen os_prep "Подготовка: утилиты" ;; - c) ui_clear; hdr "Проверка $title"; "chk_$id"; pause ;; - k) calc_menu ;; - esac; done; } -main_menu(){ local c rc - while :; do - c=$(ui_menu "demo-setup v$VERSION — домен $DOMAIN" "Какую машину настраиваем?" \ - 1 "ISP" 2 "HQ-RTR" 3 "BR-RTR" 4 "BR-FW" 5 "HQ-SW" 6 "HQ-SRV" 7 "BR-SRV" 8 "HQ-CLI" \ - 9 "Калькулятор масок" P "Параметры стенда (все адреса и имена)" 0 "Выход"); rc=$? - ((rc == 255)) && exit 0 - ((rc != 0)) && { ui_yesno "Выйти из программы?" y && { ui_clear; exit 0; }; continue; } - case $c in - 1) machine_menu isp ISP isp ;; 2) machine_menu hqr HQ-RTR hq-rtr ;; 3) machine_menu brr BR-RTR br-rtr ;; 4) machine_menu brfw BR-FW br-fw ;; - 5) machine_menu hqsw HQ-SW hq-sw ;; 6) machine_menu hqs HQ-SRV hq-srv ;; 7) machine_menu brs BR-SRV br-srv ;; 8) machine_menu hqc HQ-CLI hq-cli ;; - 9) calc_menu ;; P|p) settings_menu ;; 0) ui_clear; exit 0 ;; - esac; done; } - -ui_init -[[ -f $CONF ]] || { ui_yesno "Файл параметров не найден. Значения по умолчанию примерные — открыть параметры и сверить их с заданием?" n && settings_menu; } -main_menu