net/demosetup.sh

864 lines
67 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# =============================================================================
# demo-setup.sh v3 — помощник настройки учебного стенда ДЭ 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=3.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; }
pause(){ read -r -p "Enter — продолжить..." _ || true; }
yesno(){ local q=$1 d=${2:-n} a h; [[ $d == y ]] && h="Y/n" || h="y/N"
read -r -p "$q [$h] " a || return 1; a=${a:-$d}; [[ $a =~ ^[YyДд] ]]; }
usage(){ cat <<EOF
demo-setup.sh v$VERSION — помощник настройки стенда ДЭ 09.02.06 (RED OS 8)
bash demo-setup.sh --dry-run посмотреть меню и команды без изменений
sudo bash demo-setup.sh --apply применить выбранные пункты
Порядок работы:
1. Сделайте снимок каждой ВМ. Работайте из консоли гипервизора, не по SSH.
2. На первой ВМ откройте «Параметры», сверьте адреса и имена с заданием.
Параметры сохраняются в demo-setup.conf (права 600). Пароль не сохраняется.
3. Скопируйте скрипт и demo-setup.conf на остальные ВМ.
4. Выбирайте машину, затем пункт; пункты М1 выполняйте раньше М2 и М3.
5. После каждого пункта смотрите вывод и используйте «Проверка».
Ограничения: скрипт не проверен на настоящем стенде RED OS 8. Команды
пакетов, служб и параметры приложений из Additional.iso нужно сверить.
Перед экзаменом уточните, разрешено ли использовать скрипты.
EOF
}
[[ $MODE == help ]] && { usage; exit 0; }
if [[ $MODE == apply ]]; then
[[ $EUID -eq 0 ]] || { echo "Для --apply нужен root: sudo bash $0 --apply" >&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 ФАЙЛ [ПРАВА] <<EOF — записывает файл (с резервной копией оригинала).
put(){ local f=$1 m=${2:-644} body; body=$(cat)
if ((DRY)); then echo "${C_C}+ запись $f (права $m):${C_0}"; printf '%s\n' "$body" | sed 's/^/ | /'; return 0; fi
backup "$f"; mkdir -p "$(dirname "$f")" || return 1
(umask 077; printf '%s\n' "$body" > "$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"; }
# ask ПЕРЕМЕННАЯ "вопрос" — спрашивает значение с проверкой и сохраняет.
ask(){ local v=$1 q=$2 t a; t=$(param_type "$v"); t=${t:-text}
while :; do read -r -p "$q [${!v}]: " a || return 1; a=${a:-${!v}}
if valid "$t" "$a"; then printf -v "$v" '%s' "$a"; conf_save; return 0; fi
warn "Неверное значение (ожидается: $t)"; done; }
ask_iface(){ local v=$1 q=$2 a
echo "Интерфейсы этой ВМ:"; ip -br link 2>/dev/null | grep -vE '^(lo|ovs-system)[[:space:]]' | sed 's/^/ /'
((DRY)) && [[ -z ${!v} ]] && printf -v "$v" '%s' "eth0"
while :; do read -r -p "$q [${!v}]: " a || return 1; a=${a:-${!v}}
if is_iface "$a" || { ((DRY)) && [[ $a =~ ^[A-Za-z0-9._@-]{1,15}$ ]]; }; then printf -v "$v" '%s' "$a"; conf_save; return 0; fi
warn "Интерфейс $a не найден"; done; }
need_iface(){ [[ -n ${!1} ]] && { is_iface "${!1}" || ((DRY)); } && return 0; ask_iface "$1" "$2"; }
settings_menu(){ local i e k c
while :; do hdr "Параметры ($CONF)"; i=1
for e in "${PARAMS[@]}"; do k=${e%%|*}; printf '%3d) %-44s %s\n' "$i" "${e##*|}" "${!k}"; i=$((i+1)); done
read -r -p "Номер параметра для изменения (0 — назад): " c || return
[[ $c == 0 || -z $c ]] && return
[[ $c =~ ^[0-9]+$ ]] && ((c >= 1 && c <= ${#PARAMS[@]})) || { warn "Нет такого номера"; continue; }
e=${PARAMS[c-1]}; ask "${e%%|*}" "${e##*|}"; done; }
PASS=
need_pass(){ [[ -n $PASS ]] && return 0
if ((DRY)); then PASS='DryRun.Pass1'; return 0; fi
local p2; echo "Нужен общий пароль стенда (не сохраняется; 8–64 символа: A-Z a-z 0-9 @ . _ ! + -)."
read -r -s -p "Пароль: " PASS; echo; read -r -s -p "Повторите: " p2; echo
[[ $PASS == "$p2" && ${#PASS} -ge 8 && ${#PASS} -le 64 && $PASS =~ ^[A-Za-z0-9@._!+-]+$ ]] || { PASS=; err "Пароли не совпадают или недопустимы"; return 1; }; }
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
((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 <<EOF || return 1
# demo-setup: защищённый SSH
Port $SSH_PORT
AllowUsers $SSH_USER
MaxAuthTries 2
Banner /etc/ssh/demo-banner
PasswordAuthentication yes
EOF
sshd_apply "$SSH_PORT"; }
# Простой SSH с паролем (доступ Ansible). c_sshd_basic [ПОРТ]
c_sshd_basic(){ local port=${1:-22}; pkg openssh-server || return 1; sshd_include || return 1
put /etc/ssh/sshd_config.d/10-demo.conf 600 <<EOF || return 1
# demo-setup: SSH для Ansible
Port $port
PasswordAuthentication yes
EOF
sshd_apply "$port"; }
chrony_client(){ local srv=$1 f=/etc/chrony.conf; pkg chrony || return 1
if ((DRY)); then echo "${C_C}+ $f: закомментировать pool/server, добавить «server $srv iburst»${C_0}"
else backup "$f"; sed -i -E '/# demo-setup$/d; s/^[[:space:]]*(pool|server|peer)[[:space:]]/#&/' "$f"; echo "server $srv iburst # demo-setup" >> "$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"
ask ISP_HQ_IP "IP в сторону HQ"; ask HQ_WAN_PFX "Префикс"; ask ISP_BR_IP "IP в сторону BR"; ask BR_WAN_PFX "Префикс"
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)"
ask HQ_RTR_WAN_IP "WAN IP"; ask HQ_WAN_PFX "Префикс"; ask ISP_HQ_IP "Шлюз (ISP)"
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
ask DHCP_START "Начало пула"; ask DHCP_END "Конец пула"; ask HQ_CLI_MAC "MAC HQ-CLI для резервирования (пусто = нет)"
{ 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"
ask BR_RTR_WAN_IP "WAN IP"; ask BR_WAN_PFX "Префикс"; ask ISP_BR_IP "Шлюз (ISP)"; ask BR_RTR_FW_IP "IP к BR-FW"; ask FW_PFX "Префикс"
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"
ask BR_FW_IP "IP к BR-RTR"; ask FW_PFX "Префикс"; ask BR_RTR_FW_IP "Шлюз (BR-RTR)"; ask BR_FW_LAN_IP "IP в сети BR-SRV"; ask BR_PFX "Префикс"
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"
ask HQ_SW_MGMT_IP "IP управления"
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 <<EOF || return 1
[Unit]
Description=HQ-SW: IP управления (demo-setup)
After=openvswitch.service network-online.target
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/sbin/ip link set $HQ_SW_UP_IF up
ExecStart=/usr/sbin/ip link set $HQ_SW_SRV_IF up
ExecStart=/usr/sbin/ip link set $HQ_SW_CLI_IF up
ExecStart=/usr/sbin/ip link set hq-sw up
ExecStart=/usr/sbin/ip link set mgmt up
ExecStart=/usr/sbin/ip addr replace $HQ_SW_MGMT_IP/$MGMT_PFX dev mgmt
ExecStart=/usr/sbin/ip route replace default via $HQ_RTR_MGMT_IP
[Install]
WantedBy=multi-user.target
EOF
run systemctl daemon-reload && run systemctl enable --now demo-hqsw-mgmt || return 1
((DRY)) || ovs-vsctl show; ok "HQ-SW: транк $HQ_SW_UP_IF, access $VLAN_SRV/$VLAN_CLI, mgmt $HQ_SW_MGMT_IP"; }
chk_hqsw(){ ovs-vsctl show; ip -br a; ping -c2 -W1 "$HQ_RTR_MGMT_IP"; }
M_hqsw=("М1|Коммутатор Open vSwitch: транк, access VLAN, управление|hqsw_ovs" "М1|Часовой пояс|c_tz")
# ================================= HQ-SRV ====================================
hqs_net(){ over_ssh_warn; need_iface HQ_SRV_IF "Интерфейс HQ-SRV"
ask HQ_SRV_IP "IP"; ask SRV_PFX "Префикс"; ask HQ_RTR_SRV_IP "Шлюз (HQ-RTR)"
net_eth "$HQ_SRV_IF" static "$HQ_SRV_IP/$SRV_PFX" "$HQ_RTR_SRV_IP" "$DNS_FWD"; }
dns_records(){ if [[ -n $DNS_RECORDS ]]; then echo "$DNS_RECORDS"; return; fi
echo "hq-rtr=$HQ_RTR_SRV_IP:ptr br-rtr=$BR_RTR_FW_IP hq-srv=$HQ_SRV_IP:ptr hq-cli=$HQ_CLI_IP:ptr br-srv=$BR_SRV_IP:ptr docker=${DOCKER_DNS_IP:-$ISP_HQ_IP} web=${WEB_DNS_IP:-$ISP_BR_IP}"; }
hqs_dns(){ pkg dnsmasq || return 1; local r n ip
msg "Записи DNS (изменить: Параметры → DNS_RECORDS):"; dns_records | tr ' ' '\n' | sed 's/^/ /'
yesno "Продолжить с этими записями?" y || return 1
for r in $(dns_records); do ip=${r#*=}; ip=${ip%:ptr}; is_name "${r%%=*}" && is_ip "$ip" || { err "Плохая запись: $r"; return 1; }; done
{ echo "# demo-setup: DNS-сервер HQ-SRV"; echo "no-resolv"; echo "no-hosts"; echo "domain-needed"; echo "bogus-priv"
echo "bind-interfaces"; echo "listen-address=127.0.0.1,$HQ_SRV_IP"; echo "domain=$DOMAIN"; echo "local=/$DOMAIN/"
echo "cache-size=1000"; echo "server=$DNS_FWD"; [[ -n $DNS_FWD2 ]] && echo "server=$DNS_FWD2"
for r in $(dns_records); do n=$(fqdn "${r%%=*}"); ip=${r#*=}
if [[ $ip == *:ptr ]]; then echo "host-record=$n,${ip%:ptr}"; else echo "address=/$n/$ip"; fi; done
echo "conf-dir=/etc/dnsmasq.d,.rpmnew,.rpmsave,.rpmorig"; } | put /etc/dnsmasq.conf || return 1
run mkdir -p /etc/dnsmasq.d; ((DRY)) || dnsmasq --test || return 1
svc dnsmasq || return 1; fw_open dns
run nmcli con mod "demo-$HQ_SRV_IF" ipv4.dns 127.0.0.1 ipv4.dns-search "$DOMAIN" && run nmcli con up "demo-$HQ_SRV_IF" \
|| warn "Не удалось переключить DNS HQ-SRV на 127.0.0.1 (профиль demo-$HQ_SRV_IF)"
((DRY)) || { for n in hq-srv web docker; do getent hosts "$(fqdn $n)"; done; }; ok "DNS настроен"; }
hqs_adfwd(){ local z
for z in _msdcs _tcp _udp _sites; do echo "server=/$z.$DOMAIN/$BR_SRV_IP"; done | put /etc/dnsmasq.d/demo-ad.conf || return 1
svc dnsmasq && ok "Служебные зоны AD пересылаются на BR-SRV ($BR_SRV_IP)"; }
hqs_raid(){ pkg mdadm || return 1; local d cand=() pick=() root
[[ -e /dev/md0 ]] && { err "/dev/md0 уже существует — RAID не создаётся повторно"; return 1; }
root=$(lsblk -no PKNAME "$(findmnt -no SOURCE /)" 2>/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[*]}"
read -r -p "Диски для RAID$RAID_LEVEL через пробел: " -a pick || return 1
((${#pick[@]} >= 2)) || { err "Нужно минимум 2 диска"; return 1; }
for d in "${pick[@]}"; do [[ " ${cand[*]} " == *" $d "* ]] || { err "$d не в списке пустых дисков"; return 1; }; done
warn "ВСЕ ДАННЫЕ на ${pick[*]} будут уничтожены."
read -r -p "Введите УДАЛИТЬ для подтверждения: " d; [[ $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 <<EOF || { err "Ошибка MariaDB (если задан пароль root — выполните SQL вручную)"; return 1; }
CREATE DATABASE IF NOT EXISTS webdb;
CREATE USER IF NOT EXISTS 'web'@'localhost' IDENTIFIED BY '$PASS';
GRANT ALL PRIVILEGES ON webdb.* TO 'web'@'localhost';
FLUSH PRIVILEGES;
EOF
sql=$(find "$src" -maxdepth 2 -iname '*.sql' | head -1)
if [[ -n $sql ]]; then mysql -u root webdb < "$sql" && ok "Импортирован $sql"; else warn "SQL-дамп не найден — импортируйте вручную"; fi
backup "$root/index.php"; cp -f "$src"/* "$root"/ 2>/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; ask CERT_ALG "Алгоритм (rsa/gost)"
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
[[ -d $RAID_MOUNT/nfs ]] && run install -m 644 "$D/ca.crt" "$RAID_MOUNT/nfs/ca.crt"
ok "CA: $D; сертификаты web/docker (30 дней) — в $H/certs; ca.crt — в $RAID_MOUNT/nfs"; }
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 <<EOF || return 1
[sshd]
enabled = true
port = $SSH_PORT
backend = systemd
maxretry = 3
findtime = 10m
bantime = 1m
EOF
svc fail2ban || return 1; ((DRY)) || { sleep 2; fail2ban-client status sshd; }; }
hqs_backup(){ local f; f=$(iso_find '*yber*')
cat <<EOF
Кибер Бэкап устанавливается в основном вручную по документации:
1. На сервере управления создайте подразделение и администратора по заданию.
2. На HQ-CLI: агент и узел хранения, хранилище /backup (пункт меню HQ-CLI).
3. На HQ-SRV установите агент и зарегистрируйте его на сервере управления.
4. Создайте план резервного копирования /etc HQ-SRV в хранилище /backup и запустите.
EOF
[[ -n $f ]] && msg "Найдено в ISO: $f"; return 0; }
chk_hqs(){ ip -br a; ip r; systemctl is-active dnsmasq sshd chronyd nfs-server httpd mariadb cups 2>/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"
ask BR_SRV_IP "IP"; ask BR_PFX "Префикс"; ask BR_FW_LAN_IP "Шлюз (BR-FW)"
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"; read -r -p "Номер колонки логина: " lc; read -r -p "Колонки имени (пусто = нет): " fc
read -r -p "Колонка пароля (пусто = общий пароль): " pc; read -r -p "Колонка группы (пусто = нет): " gc
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 <<EOF || return 1
[servers]
hq-srv ansible_host=$HQ_SRV_IP ansible_user=$SSH_USER ansible_port=$SSH_PORT
[clients]
hq-cli ansible_host=$HQ_CLI_IP ansible_user=$CLI_SSH_USER ansible_port=$CLI_SSH_PORT
[routers]
hq-rtr ansible_host=$HQ_GRE_IP ansible_user=$ADM_USER ansible_port=22
br-rtr ansible_host=$BR_RTR_FW_IP ansible_user=$ADM_USER ansible_port=22
[all:vars]
ansible_password=$PASS
ansible_become=yes
ansible_become_password=$PASS
EOF
put /etc/ansible/ansible.cfg <<'EOF' || return 1
[defaults]
inventory = /etc/ansible/hosts
host_key_checking = False
interpreter_python = auto_silent
deprecation_warnings = False
EOF
((DRY)) || ansible all -m ping; warn "Файл /etc/ansible/hosts содержит пароль (права 600)."; }
brs_pcinfo(){ run mkdir -p /etc/ansible/PC-INFO || return 1
put /etc/ansible/inventory.yml <<'EOF' || return 1
---
- name: Сбор информации о хостах
hosts: hq-srv,hq-cli
gather_facts: yes
tasks:
- name: Записать имя и IP в /etc/ansible/PC-INFO/<hostname>.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)
ask APP_INNER_PORT "Внутренний порт приложения"
run install -d -m 700 /root/testapp || return 1
put /root/testapp/compose.yaml 600 <<EOF || return 1
services:
db:
image: ${db:-mariadb:latest}
container_name: db
restart: always
environment:
MARIADB_DATABASE: testdb
MARIADB_USER: test
MARIADB_PASSWORD: '$PASS'
MARIADB_ROOT_PASSWORD: '$PASS'
volumes:
- db_data:/var/lib/mysql
testapp:
image: ${site:-site:latest}
container_name: testapp
restart: always
depends_on: [db]
ports:
- "8080:$APP_INNER_PORT"
environment:
DB_TYPE: maria
DB_HOST: db
DB_NAME: testdb
DB_USER: test
DB_PASS: '$PASS'
DB_PORT: 3306
volumes:
db_data:
EOF
yesno "Открыть compose.yaml для правки перед запуском?" n && run "${EDITOR:-vi}" /root/testapp/compose.yaml
if docker compose version >/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)) || read -r -p "Путь к 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")
# ================================== меню =====================================
run_item(){ local desc=$1 fn=$2 rc
hdr "$desc"; "$fn"; rc=$?
if ((rc == 0)); then ok "Готово: $desc"; else err "Не выполнено: $desc (код $rc)"; fi; return $rc; }
machine_menu(){ local id=$1 title=$2 host=$3; local -n items="M_$id"; local i e c m d fn mm
MACHINE=$host
while :; do hdr "$title — $(hostname 2>/dev/null)"; i=1
for e in "${items[@]}"; do IFS='|' read -r m d _ <<< "$e"; printf '%3d) [%s] %s\n' "$i" "$m" "$d"; i=$((i+1)); done
echo " h) Имя хоста → $(fqdn "$host") p) Подготовка (утилиты) c) Проверка"
echo " m1/m2/m3) Выполнить модуль по порядку 0) Назад"
read -r -p "Выбор: " c || return
case $c in
0|"") return ;;
h) yesno "Сменить имя хоста на $(fqdn "$host")?" y && run_item "Имя хоста" c_hostname; pause ;;
p) run_item "Подготовка" os_prep; pause ;;
c) hdr "Проверка $title"; "chk_$id"; pause ;;
m[123]|М[123]) m="М${c:1}"
for e in "${items[@]}"; do IFS='|' read -r mm d fn <<< "$e"; [[ $mm == "$m" ]] || continue
yesno "Выполнить «$d»?" y || continue
run_item "$d" "$fn" || { yesno "Ошибка. Продолжить модуль?" n || break; }; done; pause ;;
*) if [[ $c =~ ^[0-9]+$ ]] && ((c >= 1 && c <= ${#items[@]})); then IFS='|' read -r m d fn <<< "${items[c-1]}"
yesno "Выполнить «$d»?" y && run_item "$d" "$fn"; pause
else warn "Нет такого пункта"; fi ;;
esac; done; }
main_menu(){ local c
while :; do hdr "demo-setup v$VERSION — домен $DOMAIN — $([[ $MODE == apply ]] && echo ПРИМЕНЕНИЕ || echo ПРОСМОТР)"
cat <<EOF
1) ISP 2) HQ-RTR 3) BR-RTR 4) BR-FW
5) HQ-SW 6) HQ-SRV 7) BR-SRV 8) HQ-CLI
9) Параметры (IP, имена) 0) Выход
EOF
read -r -p "Какую машину настраиваем? " c || exit 0
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) settings_menu ;; 0) exit 0 ;; *) warn "Нет такого пункта" ;;
esac; done; }
[[ -f $CONF ]] || { warn "Файл параметров не найден: значения по умолчанию примерные, сверьте их с заданием."; yesno "Открыть параметры сейчас?" n && settings_menu; }
main_menu