864 lines
67 KiB
Bash
864 lines
67 KiB
Bash
#!/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
|