1571 lines
48 KiB
Bash
Executable File
1571 lines
48 KiB
Bash
Executable File
#!/bin/bash
|
||
set -eo pipefail
|
||
#set -eou pipefail
|
||
|
||
R='\e[1;31m'
|
||
AR='\e[0;31m'
|
||
|
||
G='\e[1;92m'
|
||
AG='\e[0;92m'
|
||
|
||
C='\e[1;36m' # 亮青色
|
||
AC='\e[0;36m' # 暗青色
|
||
|
||
Y='\e[1;33m' # 亮黄色
|
||
AY='\e[0;33m' # 暗黄色
|
||
|
||
H='\e[0;90m' # 暗灰色 (推荐)
|
||
LH='\e[1;31m' # 亮黑色
|
||
AH='\e[1;90m' # 亮灰色
|
||
|
||
B='\e[0;37m' # 白色
|
||
B='\e[0;30m' # 黑色
|
||
NC='\e[0m' # No Color
|
||
|
||
# ====================== 公共工具函数 ======================
|
||
# 错误输出函数
|
||
error() {
|
||
echo -e "${AR}[E] $*${NC}" >&2
|
||
exit 1
|
||
}
|
||
|
||
# 信息输出函数
|
||
info() {
|
||
echo -e "${AG}[I]${NC} ${AC}$*${NC}"
|
||
}
|
||
|
||
# 警告输出函数
|
||
warn() {
|
||
echo -e "${AY}[W] $*${NC}"
|
||
}
|
||
|
||
# 清理最后一行
|
||
cline() {
|
||
echo -ne "\r\033[K"
|
||
}
|
||
|
||
COLUMNS=$(( $(tput cols 2>/dev/null || echo 80) - 8 ))
|
||
SSEP=$(printf "%${COLUMNS}s" "" | tr " " "-")
|
||
DSEP=$(printf "%${COLUMNS}s" "" | tr " " "=")
|
||
|
||
status_dnf_packages="/tmp/status_dnf_packages"
|
||
|
||
# ====================== 全局关联数组定义(核心优化) ======================
|
||
# 全局选项:verbose/debug/help/nodes 等,支持 GLOBAL_OPTS[v] 判断
|
||
declare -A GLOBAL_OPTS=(
|
||
[v]="" # -v
|
||
[verbose]="" # --verbose
|
||
[d]="" # -d
|
||
[debug]="" # --debug
|
||
[h]="" # -h
|
||
[help]="" # --help
|
||
[nodes]="" # -n/--nodes 节点值
|
||
)
|
||
|
||
# 子命令栈(数组:存储 exec/list node/serv autofs)
|
||
CMD_STACK=()
|
||
# 公共参数集合(-开头)
|
||
PUBLIC_ARGS=()
|
||
# 子命令专属参数(--开头)
|
||
PRIVATE_ARGS=()
|
||
# 默认节点列表(会自动从 nodes 文件覆盖)
|
||
DEFAULT_NODES=("localhost")
|
||
# 全局生效节点数组(所有子命令共享)
|
||
GLOBAL_NODES=()
|
||
declare -A GLOBAL_PORT
|
||
declare -A global_env
|
||
|
||
ask() {
|
||
# 基本用法:ask "是否删除文件?" - 无默认值,必须输入 y/yes 或 n/no
|
||
# ask "是否继续?" "y" - 默认继续,直接回车相当于 y
|
||
# ask "是否继续?" "n" - 默认取消,直接回车相当于 n
|
||
# if ask "警告:即将删除所有文件,是否继续?" "n"; then
|
||
local prompt="$1"
|
||
local default="$2"
|
||
local answer
|
||
|
||
# 构建提示信息
|
||
if [ "$default" = "y" ]; then
|
||
prompt="$prompt [Y/n]: "
|
||
elif [ "$default" = "n" ]; then
|
||
prompt="$prompt [y/N]: "
|
||
else
|
||
prompt="$prompt [y/n]: "
|
||
fi
|
||
|
||
while true; do
|
||
read -p "$prompt" answer
|
||
# 如果用户直接回车,使用默认值
|
||
if [ -z "$answer" ] && [ -n "$default" ]; then
|
||
answer="$default"
|
||
fi
|
||
# 转换为小写进行比较
|
||
answer=$(echo "$answer" | tr '[:upper:]' '[:lower:]')
|
||
case "$answer" in
|
||
y|yes) return 0 ;;
|
||
n|no) return 1 ;;
|
||
*) echo "请输入 y/yes 或 n/no" ;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
load_os_envs() {
|
||
interfaces=$(ls /sys/class/net | grep -v lo)
|
||
for iface in $interfaces; do
|
||
readarray -t config <<< "$(nmcli -g IP4.ADDRESS,IP4.GATEWAY,IP4.DNS device show $iface 2>/dev/null)"
|
||
|
||
# IP地址
|
||
ip_addr=$(echo "${config[0]}" | cut -d: -f2 | cut -d'/' -f1)
|
||
global_env["${iface}_ip"]="${ip_addr:-}"
|
||
|
||
# 网关
|
||
gateway=$(echo "${config[1]}" | cut -d: -f2)
|
||
global_env["${iface}_gateway"]="${gateway:-}"
|
||
|
||
# DNS
|
||
dns=$(echo "${config[2]}" | cut -d: -f2- | tr ' ' ',' | sed 's/,$//')
|
||
global_env["${iface}_dns"]="${dns:-223.5.5.5,223.6.6.6}"
|
||
|
||
# CIDR掩码
|
||
cidr=$(nmcli -g IP4.ADDRESS device show $iface 2>/dev/null | head -1 | cut -d'/' -f2)
|
||
global_env["${iface}_mask"]="${cidr:-24}"
|
||
done
|
||
}
|
||
|
||
getFileArgs(){
|
||
# 调用模式: getFileArgs myfile "true" 未指定文件时只返回错误码
|
||
# 调用模式: result=$(getFileArgs myfile "true") 返回文件
|
||
# 调用模式: getFileArgs myfile 未指定文件时直接退出脚本,默认值: false
|
||
|
||
local -n filename=$1
|
||
local isForce=${2:-"false"} # 默认值为 "false"
|
||
|
||
# 检查 file 参数是否传入
|
||
if [[ -z "${GLOBAL_OPTS[file]-}" ]]; then
|
||
local err_msg="未指定文件,请使用 -f 或 --file 参数"
|
||
|
||
if [[ "$isForce" == "false" ]]; then
|
||
error "$err_msg"
|
||
else
|
||
warn "$err_msg"
|
||
return 1 # 返回非0表示失败
|
||
fi
|
||
fi
|
||
|
||
# 检查文件是否存在
|
||
if [[ -f "${GLOBAL_OPTS[file]}" ]] || [[ -d "${GLOBAL_OPTS[file]}" ]]; then
|
||
filename="${GLOBAL_OPTS[file]}"
|
||
return 0
|
||
else
|
||
local err_msg="错误:文件或目录不存在 -> ${GLOBAL_OPTS[file]}"
|
||
|
||
if [[ "$isForce" == "false" ]]; then
|
||
error "$err_msg"
|
||
else
|
||
warn "$err_msg"
|
||
return 1
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# ====================== 核心函数:加载环境变量信息 ==============
|
||
load_conf_envs() {
|
||
|
||
while read -r key val; do
|
||
# 跳过空行、注释行
|
||
[[ -z "$key" || "$key" == \#* ]] && continue
|
||
|
||
global_env["$key"]="$val"
|
||
done < "$config_file"
|
||
}
|
||
|
||
# ============================================================
|
||
# 辅助函数:打印所有环境变量(调试用)
|
||
# ============================================================
|
||
print_global_env() {
|
||
for key in "${!global_env[@]}"; do
|
||
printf "%-20s = %s\n" "$key" "${global_env[$key]}"
|
||
done
|
||
echo "======================================"
|
||
}
|
||
|
||
# ============================================================
|
||
# 辅助函数:获取指定网卡IP
|
||
# ============================================================
|
||
get_interface_ip() {
|
||
local interface="$1"
|
||
if ip link show "$interface" &>/dev/null; then
|
||
ip addr show "$interface" | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | head -1
|
||
else
|
||
echo ""
|
||
fi
|
||
}
|
||
|
||
# ============================================================
|
||
# 辅助函数:获取指定网卡MAC地址
|
||
# ============================================================
|
||
get_interface_mac() {
|
||
local interface="$1"
|
||
if ip link show "$interface" &>/dev/null; then
|
||
ip link show "$interface" | grep -oP '(?<=link/ether\s)[0-9a-f:]+' | head -1
|
||
else
|
||
echo ""
|
||
fi
|
||
}
|
||
|
||
# ============================================================
|
||
# 辅助函数:快速获取系统信息摘要
|
||
# ============================================================
|
||
show_system_summary() {
|
||
cat <<EOF
|
||
========================================
|
||
系统信息摘要
|
||
========================================
|
||
操作系统: ${global_env[release]} (${global_env[osname]} ${global_env[os_version]})
|
||
内核版本: ${global_env[kernel_release]}
|
||
主机名: ${global_env[hostname]}
|
||
架构: ${global_env[architecture]}
|
||
CPU: ${global_env[cpu_model]}
|
||
CPU核心数: ${global_env[cpu_cores]}
|
||
内存总量: ${global_env[mem_total]} GB
|
||
默认网卡: ${global_env[default_interface]}
|
||
系统负载: ${global_env[load_1min]} (1min) / ${global_env[load_5min]} (5min)
|
||
磁盘使用: ${global_env[disk_root_usage]}% (根分区)
|
||
虚拟化: ${global_env[virt_type]}
|
||
当前用户: ${global_env[username]} (UID: ${global_env[uid]})
|
||
========================================
|
||
EOF
|
||
}
|
||
|
||
# ============================================================
|
||
# 辅助函数:通用函数
|
||
# ============================================================
|
||
|
||
# 通用命令执行函数,带进度显示和日志记录
|
||
# 用法: run_with_progress "命令描述" "要执行的命令" [日志文件]
|
||
# 示例: run_with_progress "编译SLURM" "rpmbuild -ta slurm.tar.gz" "slurm_build.log"
|
||
run_with_progress() {
|
||
local description="$1"
|
||
local command="$2"
|
||
local log_file="${3:-/tmp/command_$(date +%Y%m%d_%H%M%S).log}"
|
||
local count=0
|
||
local start_time=$(date +%s)
|
||
local exit_code=0
|
||
|
||
info "$DSEP"
|
||
info "开始: $description"
|
||
info "日志: $log_file"
|
||
info "$DSEP"
|
||
|
||
# 执行命令,同时保存日志和显示进度
|
||
eval "$command" 2>&1 | tee "$log_file" | while IFS= read -r line; do
|
||
count=$((count + 1))
|
||
local current_time=$(date +%s)
|
||
local elapsed=$((current_time - start_time))
|
||
local elapsed_min=$((elapsed / 60))
|
||
local elapsed_sec=$((elapsed % 60))
|
||
|
||
# 提取关键信息(去掉路径等冗余信息)
|
||
local short_line=$(echo "$line" | sed 's|/home/[^/]*/|~/|g' | cut -c1-60)
|
||
|
||
printf "\r${AC}[B] [%s]${NR} ${H}[%d]${NR} %02d:%02d - %.60s" \
|
||
"$description" $count $elapsed_min $elapsed_sec "$short_line"
|
||
done
|
||
|
||
exit_code=${PIPESTATUS[0]}
|
||
|
||
# 清除最后一行进度信息
|
||
echo -ne "\r\033[K"
|
||
|
||
if [ $exit_code -eq 0 ]; then
|
||
info "[$description] - 完成! (共 $count 步, 耗时 $(($(date +%s) - start_time))秒)"
|
||
info "日志已保存: $log_file"
|
||
else
|
||
warn "❌ $description - 失败! (退出码: $exit_code, 共 $count 步)"
|
||
warn "请查看日志: $log_file"
|
||
return $exit_code
|
||
fi
|
||
}
|
||
|
||
# 过滤安装包
|
||
filter_pkgs(){
|
||
local -n input_array=$1 # 输入的包数组
|
||
local -n output_array=$2 # 输出的包数组
|
||
|
||
output_array=() # 清空输出数组
|
||
for pkg in ${input_array[@]}; do
|
||
#grep -q $pkg $status_dnf_packages && continue
|
||
if ! rpm -q "$pkg" &>/dev/null; then
|
||
#echo "$pkg" >> $status_dnf_packages
|
||
output_array+=("$pkg")
|
||
else
|
||
info "Skip installation, already installed - $pkg"
|
||
fi
|
||
done
|
||
}
|
||
|
||
cidr_to_netmask() {
|
||
local cidr=$1
|
||
local masks=(
|
||
"0.0.0.0" # 0
|
||
"128.0.0.0" # 1
|
||
"192.0.0.0" # 2
|
||
"224.0.0.0" # 3
|
||
"240.0.0.0" # 4
|
||
"248.0.0.0" # 5
|
||
"252.0.0.0" # 6
|
||
"254.0.0.0" # 7
|
||
"255.0.0.0" # 8
|
||
"255.128.0.0" # 9
|
||
"255.192.0.0" # 10
|
||
"255.224.0.0" # 11
|
||
"255.240.0.0" # 12
|
||
"255.248.0.0" # 13
|
||
"255.252.0.0" # 14
|
||
"255.254.0.0" # 15
|
||
"255.255.0.0" # 16
|
||
"255.255.128.0" # 17
|
||
"255.255.192.0" # 18
|
||
"255.255.224.0" # 19
|
||
"255.255.240.0" # 20
|
||
"255.255.248.0" # 21
|
||
"255.255.252.0" # 22
|
||
"255.255.254.0" # 23
|
||
"255.255.255.0" # 24
|
||
"255.255.255.128" # 25
|
||
"255.255.255.192" # 26
|
||
"255.255.255.224" # 27
|
||
"255.255.255.240" # 28
|
||
"255.255.255.248" # 29
|
||
"255.255.255.252" # 30
|
||
"255.255.255.254" # 31
|
||
"255.255.255.255" # 32
|
||
)
|
||
echo "${masks[$cidr]}"
|
||
}
|
||
|
||
get_subnet_address() {
|
||
local ip="$1"
|
||
local mask="$2"
|
||
|
||
IFS=. read -r i1 i2 i3 i4 <<< "$ip"
|
||
IFS=. read -r m1 m2 m3 m4 <<< "$mask"
|
||
|
||
n1=$((i1 & m1))
|
||
n2=$((i2 & m2))
|
||
n3=$((i3 & m3))
|
||
n4=$((i4 & m4))
|
||
|
||
echo "$n1.$n2.$n3.$n4"
|
||
|
||
}
|
||
# ====================== pxe server functions ====================
|
||
pxe_dhcp_build() {
|
||
log_debug "当前节点:${GLOBAL_NODES[*]}"
|
||
log_debug "公共参数:${PUBLIC_ARGS[*]}"
|
||
log_debug "私有参数:${PRIVATE_ARGS[*]}"
|
||
|
||
interface="${GLOBAL_OPTS[interface]}"
|
||
ipaddr=${global_env[${interface}_ip]}
|
||
netcidr=${global_env[${interface}_mask]}
|
||
netmask=$(cidr_to_netmask 24)
|
||
subnet=$(get_subnet_address $ipaddr $netmask)
|
||
|
||
IFS=. read -r i1 i2 i3 i4 <<< "$ipaddr"
|
||
start_ip="$i1.$i2.$i3.10"
|
||
ender_ip="$i1.$i2.$i3.90"
|
||
tests_ip="$i1.$i2.$i3.252"
|
||
|
||
info "DHCP Interface name: $interface"
|
||
info "DHCP IP Address : $ipaddr"
|
||
info "DHCP IP Netmask : $netmask"
|
||
info "DHCP IP Subnets : $subnet"
|
||
info "DHCP Start IPAddr : $start_ip"
|
||
info "DHCP End IPAddr : $ender_ip"
|
||
|
||
nopkgs=()
|
||
pkgs=("dhcp-server dhcp-common")
|
||
filter_pkgs pkgs nopkgs
|
||
|
||
local cmd=("dnf -y install ${nopkgs[*]}")
|
||
[[ -z ${nopkgs[@]} ]] || run_with_progress "DNF 安装DHCP服务..." "$cmd" "$log_file"
|
||
|
||
dhcp_config="/etc/dhcp/dhcpd.conf"
|
||
|
||
{
|
||
echo "option domain-name \"`hostname -s`\";"
|
||
echo "option domain-name-servers 8.8.8.8;"
|
||
echo "default-lease-time 600;"
|
||
echo "max-lease-time 7200;"
|
||
echo "ddns-update-style none;"
|
||
echo ""
|
||
echo "allow booting;"
|
||
echo "allow bootp;"
|
||
echo ""
|
||
echo "option arch code 93 = unsigned integer 16;"
|
||
echo ""
|
||
echo "subnet ${subnet} netmask ${netmask} {"
|
||
echo " option routers ${ipaddr};"
|
||
echo " range $start_ip $ender_ip;"
|
||
echo ""
|
||
echo " next-server ${ipaddr};"
|
||
echo ""
|
||
echo ' host test_host_bond {'
|
||
echo ' option dhcp-client-identifier "test_host_bond";'
|
||
echo " fixed-address $tests_ip;"
|
||
echo ' }'
|
||
echo ' if option arch= 00:07 or option arch = 00:09 {'
|
||
echo ' # UEFI x64'
|
||
echo ' filename "/EFI/x86/grub.efi";'
|
||
echo ' } else if option arch = 00:0b {'
|
||
echo ' # UEFI ARM64'
|
||
echo ' filename "/EFI/aarch64/bootaa64.efi";'
|
||
echo ' } else {'
|
||
echo ' # Legcy BIOS'
|
||
echo ' filename "pxelinux.0";'
|
||
echo ' }'
|
||
echo '}'
|
||
} > ${dhcp_config}
|
||
|
||
# sed -i 's/\$DHCPDARGS/eth0/' /usr/lib/systemd/system/dhcpd.service
|
||
dhcp_systemd="/usr/lib/systemd/system/dhcpd.service"
|
||
grep -q "DHCPDARGS" $dhcp_systemd &>/dev/null && \
|
||
sed -i "s/\$DHCPDARGS/${interface}/" $dhcp_systemd &>/dev/null
|
||
|
||
systemctl daemon-reload && systemctl restart dhcpd.service
|
||
systemctl is-enabled dhcpd &>/dev/null || systemctl enable dhcpd.service &>/dev/null
|
||
|
||
systemctl is-active dhcpd &>/dev/null && info "DHCP 服务已经配置成功..." \
|
||
|| warn "DHCP 服务部署失败,请查看服务信息 systemctl status dhcpd.service"
|
||
|
||
}
|
||
|
||
pxe_tftp_build() {
|
||
log_debug "当前节点:${GLOBAL_NODES[*]}"
|
||
log_debug "公共参数:${PUBLIC_ARGS[*]}"
|
||
log_debug "私有参数:${PRIVATE_ARGS[*]}"
|
||
|
||
nopkgs=()
|
||
pkgs=("tftp-server tftp syslinux")
|
||
filter_pkgs pkgs nopkgs
|
||
|
||
local cmd=("dnf -y install ${nopkgs[*]}")
|
||
[[ -z ${nopkgs[@]} ]] || run_with_progress "DNF 安装TFTP服务..." "$cmd" "$log_file"
|
||
|
||
tftp_server_conf="/usr/lib/systemd/system/tftp.service"
|
||
tftp_socket_conf="/usr/lib/systemd/system/tftp.socket"
|
||
tftp_pxe_dirs="/var/lib/tftpboot"
|
||
|
||
[[ -e "$tftp_pxe_dirs" ]] || mkdir -p "$tftp_pxe_dirs/pxelinux.cfg"
|
||
chmod -R 755 $tftp_pxe_dirs
|
||
|
||
# Pxe 启动文件配置
|
||
src_pxelinux="/usr/share/syslinux"
|
||
src_files=("pxelinux.0 menu.c32 ldlinux.c32")
|
||
for fname in ${src_files[@]}; do
|
||
src_file="$src_pxelinux/$fname"
|
||
dst_file="$tftp_pxe_dirs/$fname"
|
||
|
||
[[ -f $src_file ]] || warn "$src_file not found!"
|
||
|
||
if [[ -f $dst_file ]]; then
|
||
info "The $fname already exists, Skipt shis copy..."
|
||
else
|
||
info "Copying the $fname to $dst_file"
|
||
/usr/bin/cp $src_file $dst_file
|
||
fi
|
||
done
|
||
|
||
# 文件配置
|
||
if ! getFileArgs myfile "true"; then
|
||
warn " 必须指定系统ISO文件或挂载位置,例如: "
|
||
warn " -f /root/Rocky-9.7-x86_64-dvd.iso 或 -f /mnt/cdrom/"
|
||
exit 1
|
||
fi
|
||
|
||
echo "------------> $myfile"
|
||
|
||
exit 1
|
||
|
||
# -c 允许创建新文件(上传) -p 使用普通系统权限检查
|
||
{
|
||
echo '[Unit]'
|
||
echo 'Description=Tftp Server'
|
||
echo 'Requires=tftp.socket'
|
||
echo 'Documentation=man:in.tftpd'
|
||
echo ''
|
||
echo '[Service]'
|
||
echo "ExecStart=/usr/sbin/in.tftpd -c -p -s "$tftp_pxe_dirs""
|
||
echo 'StandardInput=socket'
|
||
echo ''
|
||
echo '[Install]'
|
||
echo 'WantedBy=multi-user.target'
|
||
echo 'Also=tftp-server.socket'
|
||
} > ${tftp_server_conf}
|
||
|
||
systemctl daemon-reload && systemctl restart tftp.socket
|
||
systemctl is-enabled tftp.socket &>/dev/null || systemctl enable tftp.socket &>/dev/null
|
||
|
||
info "TFTP 服务端口: 69"
|
||
systemctl is-active tftp.socket &>/dev/null && info "TFTP 服务已经配置成功..." \
|
||
|| warn "TFTP 服务部署失败,请查看服务信息 systemctl status tftp.socket"
|
||
}
|
||
|
||
pxe_http_build() {
|
||
log_debug "当前节点:${GLOBAL_NODES[*]}"
|
||
log_debug "公共参数:${PUBLIC_ARGS[*]}"
|
||
log_debug "私有参数:${PRIVATE_ARGS[*]}"
|
||
|
||
|
||
nopkgs=()
|
||
pkgs=("httpd")
|
||
filter_pkgs pkgs nopkgs
|
||
|
||
local cmd=("dnf -y install ${nopkgs[*]}")
|
||
[[ -z ${nopkgs[@]} ]] || run_with_progress "DNF 安装HTTPD服务..." "$cmd" "$log_file"
|
||
|
||
http_root_conf="/etc/httpd"
|
||
http_html_dirs="/var/www/html"
|
||
http_serv_conf="/usr/lib/systemd/system/httpd.service"
|
||
|
||
custom_conf="$http_root_conf/conf.d/sunhpc.conf"
|
||
{
|
||
echo 'Alias /rocky97 /var/www/html/rocky97'
|
||
echo ''
|
||
echo 'UseCanonicalName Off'
|
||
echo "ServerName `hostname -f`"
|
||
echo ''
|
||
echo "<Directory \"$rocky_dirs\">"
|
||
echo " Options Indexes FollowSymLinks"
|
||
echo " AllowOverride None"
|
||
echo " Require all granted"
|
||
echo ""
|
||
echo " EnableSendfile on"
|
||
echo "</Directory>"
|
||
} > ${custom_conf}
|
||
|
||
#systemctl daemon-reload && systemctl restart httpd
|
||
#systemctl is-enabled httpd &>/dev/null || systemctl enable httpd &>/dev/null
|
||
|
||
|
||
info "HTTP 服务端口: 80,443"
|
||
if systemctl is-active httpd &>/dev/null ; then
|
||
info "HTTP 服务已经配置成功..."
|
||
info "后续配置: "
|
||
info " 拷贝系统ISO文件到 $rocky_repo_src"
|
||
info " # rsync -ah --info=progress2 /mnt/ /srv/repos/rocky/9.7/"
|
||
else
|
||
warn "HTTP 服务部署失败,请查看服务信息 systemctl status httpd.service"
|
||
fi
|
||
|
||
if getFileArgs myfile "true"; then
|
||
local mnt_iso_dirs=$myfile
|
||
if [[ -f $myfile ]]; then
|
||
mnt_dst_dirs="/mnt/cdrom"
|
||
[[ -e "$mnt_dst_dirs" ]] && umount $mnt_dst_dirs &>/dev/null || mkdir -p $mnt_dst_dirs
|
||
info "Mount path: $mnt_dst_dirs"
|
||
mount $myfile $mnt_dst_dirs &>/dev/null
|
||
mnt_iso_dirs=$mnt_dst_dirs
|
||
fi
|
||
|
||
rpmname=$(find $mnt_iso_dirs -iname "rocky-release-*.rpm")
|
||
filename=$(basename "$rpmname" .rpm)
|
||
distro=$(echo "$filename" | cut -d'-' -f1) # rocky
|
||
version=$(echo "$filename" | cut -d'-' -f3) # 9.7
|
||
distro=${distro:-"linux"}
|
||
version=${version:-"1.0"}
|
||
|
||
rocky_repo_distro="/srv/repos/$distro"
|
||
rocky_repo_version="$rocky_repo_distro/$version"
|
||
rocky_html_dst="$http_html_dirs/$distro"
|
||
[[ ! -e $rocky_repo_version ]] && mkdir -p $rocky_repo_version
|
||
[[ ! -e $rocky_html_dst ]] && ln -s $rocky_repo_distro $rocky_html_dst
|
||
|
||
info "拷贝系统镜像到本地: $rocky_repo_src"
|
||
info " 系统发行版: $distro"
|
||
info " 系统版本 : $version"
|
||
info " 源地址 : $mnt_iso_dirs"
|
||
info " 目的地址 : $rocky_repo_version"
|
||
|
||
rsync -ah --info=progress2 "$mnt_iso_dirs" "$rocky_repo_version/"
|
||
info "测试访问: curl -L http://`hostname -s`/$distro/$version/"
|
||
else
|
||
info "后续配置: "
|
||
info " 拷贝系统ISO文件到 $rocky_repo_src"
|
||
info " # rsync -ah --info=progress2 /mnt/ /srv/repos/rocky/9.7/"
|
||
fi
|
||
}
|
||
|
||
pxe_dnsmasq_build() {
|
||
log_debug "当前节点:${GLOBAL_NODES[*]}"
|
||
log_debug "公共参数:${PUBLIC_ARGS[*]}"
|
||
log_debug "私有参数:${PRIVATE_ARGS[*]}"
|
||
|
||
nopkgs=()
|
||
pkgs=("syslinux dnsmasq dnsmasq-utils ipxe-bootimgs")
|
||
filter_pkgs pkgs nopkgs
|
||
|
||
local cmd=("dnf -y install ${nopkgs[*]}")
|
||
[[ -z ${nopkgs[@]} ]] || run_with_progress "DNF 安装 dnsmasq 服务..." "$cmd" "$log_file"
|
||
|
||
# -n 非空True,-z 为空True
|
||
[[ -n ${global_env[interface]} ]] && iface=${global_env[interface]} || \
|
||
error "请指定DHCP监听接口命名, 编辑 $config_file 文件, 例如: interface eth1"
|
||
|
||
[[ -n ${global_env[server]} ]] && ipaddr=${global_env[server]} || \
|
||
error "请指定监听接口IP地址, 编辑 $config_file 文件, 例如: server 172.16.9.254"
|
||
|
||
[[ -n ${global_env[dns]} ]] && dnsip=${global_env[dns]} || dnsip="223.5.5.5"
|
||
|
||
[[ -n ${global_env[range]} ]] && iprange=${global_env[range]} || \
|
||
error "请指定DHCP网络IP地址范围, 编辑 $config_file 文件, 例如: range 172.16.9.1,172.16.9.100"
|
||
|
||
vmlinuz="http://$ipaddr/rocky/isolinux/vmlinuz"
|
||
[[ -n ${global_env[vmlinuz]} ]] && vmlinuz=${global_env[vmlinuz]} || \
|
||
error "请指定vmlinuz web路径, 编辑 $config_file 文件, 例如: vmlinuz http://$ipaddr/rocky/isolinux/vmlinuz"
|
||
|
||
initrd="http://$ipaddr/rocky/isolinux/initrd.img"
|
||
[[ -n ${global_env[initrd]} ]] && initrd=${global_env[initrd]} || \
|
||
error "请指定initrd web路径, 编辑 $config_file 文件, 例如: initrd http://$ipaddr/rocky/isolinux/initrd.img"
|
||
[[ -n ${global_env[ks]} ]] && ks=${global_env[ks]} || \
|
||
error "请指定ks web路径, 编辑 $config_file 文件, 例如: ks http://$ipaddr/ks/ks.ks"
|
||
[[ -n ${global_env[repo]} ]] && repo=${global_env[repo]} || \
|
||
error "请指定ks web路径, 编辑 $config_file 文件, 例如: repo http://$ipaddr/rocky/"
|
||
|
||
kargs="net.ifnames=0 biosdevname=0"
|
||
[[ -n ${global_env[kargs]} ]] && kargs="$kargs ${global_env[kargs]}"
|
||
|
||
ipxeboot="http://$ipaddr/ks/boot.ipxe"
|
||
[[ -n ${global_env[bootipxe]} ]] && ipxeboot=${global_env[bootipxe]}
|
||
|
||
htmlroot="/var/www/html"
|
||
[[ -n ${global_env[htmlroot]} ]] && htmlroot=${global_env[htmlroot]}
|
||
|
||
|
||
boot_root="/srv/pxelinux/tftpboot"
|
||
dnsmasq_conf="/etc/dnsmasq.d/sunhpc.conf"
|
||
|
||
[[ -e "$boot_root/boot" ]] || mkdir -p "$boot_root/boot"
|
||
{
|
||
echo "interface=$interface"
|
||
echo "bind-interfaces"
|
||
echo ""
|
||
echo "enable-tftp"
|
||
echo "tftp-root=${boot_root}"
|
||
echo ""
|
||
echo "# Gateway:3, DNS:6"
|
||
echo "dhcp-range=$iprange,12h"
|
||
echo "dhcp-option=3,${ipaddr}"
|
||
echo "dhcp-option=6,$dnsip"
|
||
echo ""
|
||
echo "dhcp-match=set:bios,option:client-arch,0"
|
||
echo "dhcp-match=set:uefi,option:client-arch,7"
|
||
echo "dhcp-match=set:uefi,option:client-arch,9"
|
||
echo ""
|
||
echo "dhcp-boot=tag:bios,boot/undionly.kpxe"
|
||
echo "dhcp-boot=tag:uefi,boot/bootx64.efi"
|
||
echo "dhcp-boot=tag:ipxe,$ipxeboot"
|
||
} > ${dnsmasq_conf}
|
||
|
||
ipxeconf="$htmlroot$(echo "$htmlroot" | cut -d'/' -f4-)"
|
||
echo "----ipxeconf----> $ipxeconf"
|
||
|
||
}
|
||
# ====================== server functions ========================
|
||
slurm_build() {
|
||
log_debug "当前节点:${GLOBAL_NODES[*]}"
|
||
log_debug "公共参数:${PUBLIC_ARGS[*]}"
|
||
log_debug "私有参数:${PRIVATE_ARGS[*]}"
|
||
|
||
FILE_CONF="slurm_build.conf"
|
||
REPOS_DIRS="/etc/yum.repos.d/*.repo"
|
||
SEARCH_STR="mirrors.ustc.edu.cn"
|
||
log_file="slurm_build.log"
|
||
EPEL_C="/etc/yum.repos.d/epel.repo"
|
||
REPO_C="/etc/yum.repos.d/slurm.repo"
|
||
DST_DIR="/srv/repos/slurm/packages"
|
||
|
||
STATUS="$DST_DIR/.installed"
|
||
init_db_flags="/var/lib/mysql/.initialized"
|
||
slurmctld_flags="/etc/slurm/.slurmctld"
|
||
slurmdbd_flags="/etc/slurm/.slurmdbd"
|
||
slurmd_flags="/etc/slurm/.slurmd"
|
||
|
||
|
||
# 检查 file 参数是否传入
|
||
if [[ -z "${GLOBAL_OPTS[file]-}" ]]; then
|
||
echo "错误:未指定文件,请使用 -f 或 --file 参数"
|
||
exit 1
|
||
fi
|
||
|
||
# 检查文件是否存在
|
||
if [[ ! -f "${GLOBAL_OPTS[file]}" ]]; then
|
||
echo "错误:文件不存在 → ${GLOBAL_OPTS[file]}"
|
||
exit 1
|
||
fi
|
||
|
||
info "${DSEP}"
|
||
info "🔨 开始 Slurm 构建安装(断点续跑模式)"
|
||
info "${DSEP}"
|
||
|
||
dnf config-manager --enable crb
|
||
[ -f "$EPEL_C" ] && info "EPEL Repo is enabled" || dnf -y install epel-release
|
||
|
||
local cmd="dnf -y install @development yum-utils dnf-utils rpm-build"
|
||
[[ -f $STATUS ]] || run_with_progress "DNF 安装系统开发依赖包..." "$cmd" "$log_file"
|
||
|
||
local pkgs="munge munge-devel mariadb mariadb-devel \
|
||
gtk2 gtk2-devel gtk3 gtk3-devel http-parser http-parser-devel \
|
||
json-c json-c-devel libyaml libyaml-devel libjwt libjwt-devel \
|
||
wget python3 readline-devel pam-devel perl-ExtUtils-MakeMaker \
|
||
perl-devel perl-JSON-PP createrepo_c hdf5 hdf5-devel man2html \
|
||
man2html-core pam pam-devel freeipmi freeipmi-devel numactl \
|
||
numactl-devel pmix pmix-devel hwloc hwloc-devel lua lua-devel \
|
||
ucx ucx-devel jq lz4-devel mariadb-server mariadb-devel"
|
||
|
||
nopkgs=()
|
||
filter_pkgs pkgs nopkgs
|
||
|
||
cmd=("dnf -y install ${nopkgs[*]}")
|
||
[[ -z ${nopkgs[@]} ]] || run_with_progress "DNF 安装系统基础依赖包..." "$cmd" "$log_file"
|
||
|
||
export CPPFLAGS="-I$PWD/deps_pkgs/cuda-13.0/targets/x86_64-linux/include ${CPPFLAGS}"
|
||
export LDFLAGS="-L$PWD/deps_pkgs/cuda-13.0/targets/x86_64-linux/lib/stubs ${LDFLAGS}"
|
||
|
||
[[ -f $STATUS ]] && info "Slurm 已经存在 ..." || info "开始制作 Slurm rpm 安装包 ..."
|
||
|
||
TARFILE=${GLOBAL_OPTS[file]}
|
||
local cmd="rpmbuild -ta \
|
||
--with slurmrestd --with hdf5 --with hwloc --with muma \
|
||
--with pmix --with nvml --with lua --with ucx --with jwt --with freeipmi \
|
||
$TARFILE"
|
||
|
||
[[ -f $STATUS ]] || run_with_progress "Slurm RPM 构建..." "$cmd" "$log_file"
|
||
|
||
mkdir -pv $DST_DIR
|
||
find $HOME/rpmbuild/RPMS/ -iname "*.rpm" -exec mv {} $DST_DIR \;
|
||
|
||
{
|
||
echo "[slurm]"
|
||
echo "name=Slurm Local Repository"
|
||
echo "baseurl=file:///$DST_DIR"
|
||
echo "gpgcheck=0"
|
||
echo "enabled=1"
|
||
} > "$REPO_C"
|
||
touch $DST_DIR/.installed
|
||
|
||
#createrepo_c $DST_DIR 2>&1 | tee $log_file
|
||
info "SLURM 部署完成, Repos: $DST_DIR"
|
||
|
||
local pkgs="pcp-pmda-slurm slurm slurm-perlapi \
|
||
slurm-contribs slurm-devel slurm-libpmi slurm-pam_slurm \
|
||
slurm-sackd slurm-slurmctld slurm-slurmd \
|
||
slurm-slurmdbd slurm-slurmrestd \
|
||
slurm-example-configs"
|
||
|
||
nopkgs=()
|
||
filter_pkgs pkgs nopkgs
|
||
|
||
cmd=("dnf -y install ${nopkgs[*]}")
|
||
[[ -z ${nopkgs[@]} ]] || run_with_progress "Slurm 服务安装..." "$cmd" "$log_file"
|
||
|
||
# 开始配置 MariaDB 数据库
|
||
slurm_db_conf="/etc/my.cnf.d/slurm.cnf"
|
||
{
|
||
echo "[mysqld]"
|
||
echo "innodb_buffer_pool_size = 2G"
|
||
echo "innodb_log_file_size = 64M"
|
||
echo "innodb_lock_wait_timeout = 900"
|
||
} > "$slurm_db_conf"
|
||
systemctl is-active mariadb &>/dev/null || systemctl start mariadb &>/dev/null
|
||
systemctl is-enabled mariadb &>/dev/null || systemctl enable mariadb &>/dev/null
|
||
|
||
DB_ROOT_PASSWORD="admin_b101"
|
||
# 静默执行初始化数据库: mysql_secure_installation
|
||
[[ -f $init_db_flags ]] && info "Database already initialized, skipping mysql_secure_installation" \
|
||
|| mysql_secure_installation <<EOF
|
||
|
||
n
|
||
y
|
||
${DB_ROOT_PASSWORD}
|
||
${DB_ROOT_PASSWORD}
|
||
y
|
||
y
|
||
y
|
||
y
|
||
EOF
|
||
if mysql -u root -p${DB_ROOT_PASSWORD} -e "select 1" >/dev/null 2>&1; then
|
||
info "MariaDB initialization and securing completed successfully!"
|
||
else
|
||
warn "Error: Authentication failed. Please check the logs."
|
||
fi
|
||
info "数据库初始化密码: $DB_ROOT_PASSWORD"
|
||
info "重新设置密码: "
|
||
info " systemctl stop mariadb"
|
||
info " mysqld_safe --skip-grant-tables &"
|
||
info " mysql -u root"
|
||
info " flush privileges;"
|
||
info " alter user 'root'@'localhost' identified by 'new_pass'"
|
||
info " exit"
|
||
info "killall mysqld"
|
||
info "systemctl start mariadb"
|
||
|
||
SLURM_DB_PW="admin_b101"
|
||
SLURM_DB_NM="slurm_acct_db"
|
||
|
||
info "开始创建 Slurm 基础数据库: ${SLURM_DB_NM}"
|
||
info "Slurm Database User: slurm"
|
||
info "Slurm Database Pass: $SLURM_DB_PW"
|
||
|
||
[[ -f $init_db_flags ]] || mysql -u root -p${DB_ROOT_PASSWORD} <<-EOSQL
|
||
CREATE DATABASE IF NOT EXISTS ${SLURM_DB_NM};
|
||
DROP USER IF EXISTS 'slurm'@'localhost';
|
||
CREATE USER IF NOT EXISTS 'slurm'@'localhost' identified by '${SLURM_DB_PW}';
|
||
GRANT ALL PRIVILEGES ON ${SLURM_DB_NM}.* TO 'slurm'@'localhost' WITH GRANT OPTION;
|
||
FLUSH PRIVILEGES;
|
||
EOSQL
|
||
touch ${init_db_flags}
|
||
|
||
# 配置 slurmdbd 服务
|
||
slurm_root="/etc/slurm"
|
||
slurm_dbdc="$slurm_root/slurmdbd.conf"
|
||
slurm_ctld="$slurm_root/slurm.conf"
|
||
getent passwd slurm &>/dev/null || useradd -r -b /var/lib -s /sbin/nologin slurm
|
||
getent passwd slurm &>/dev/null || useradd -r -b /var/lib -s /sbin/nologin slurmrestd
|
||
|
||
touch ${slurm_dbdc}
|
||
chown slurm:slurm ${slurm_dbdc}
|
||
chmod 600 ${slurm_dbdc}
|
||
|
||
mkdir -p "/var/log/slurm"
|
||
mkdir -p "/var/run/slurmdbd"
|
||
chown -R slurm:slurm "/var/log/slurm"
|
||
chown -R slurm:slurm "/var/run/slurmdbd"
|
||
|
||
[[ -f $slurmdbd_flags ]] && info "SlurmDBD Service is configuration finisheld..." \
|
||
|| cat > ${slurm_dbdc} <<-EOSLURMDBD
|
||
AuthType=auth/munge
|
||
LogFile=/var/log/slurm/slurmdbd.log
|
||
PidFile=/var/run/slurmdbd/slurmdbd.pid
|
||
|
||
DbdHost=`hostname -s`
|
||
DbdPort=6819
|
||
SlurmUser=slurm
|
||
DebugLevel=info
|
||
|
||
StorageType=accounting_storage/mysql
|
||
StorageHost=localhost
|
||
StoragePort=3306
|
||
StorageUser=slurm
|
||
StoragePass=${SLURM_DB_PW}
|
||
StorageLoc=${SLURM_DB_NM}
|
||
|
||
ArchiveEvents=yes
|
||
ArchiveJobs=yes
|
||
ArchiveResvs=yes
|
||
ArchiveSteps=no
|
||
ArchiveSuspend=no
|
||
ArchiveTXN=no
|
||
ArchiveUsage=no
|
||
PurgeEventAfter=1month
|
||
PurgeJobAfter=12month
|
||
PurgeResvAfter=1month
|
||
PurgeStepAfter=1month
|
||
PurgeSuspendAfter=1month
|
||
PurgeTXNAfter=12month
|
||
PurgeUsageAfter=24month
|
||
EOSLURMDBD
|
||
touch $slurmdbd_flags
|
||
|
||
if [[ ! -f $slurmdbd_flags ]]; then
|
||
systemctl restart slurmdbd &>/dev/null
|
||
systemctl is-enabled slurmdbd &>/dev/null || systemctl enable slurmdbd &>/dev/null
|
||
fi
|
||
|
||
# 配置 slurmctl 服务
|
||
nodeinfo=$(slurmd -C | head -1)
|
||
[[ -f $slurmctld_flags ]] && info "SlurmCTLD Service is configuration finisheld..." \
|
||
|| cat > ${slurm_ctld} <<-EOSLURMCTLD
|
||
ClusterName=cluster
|
||
ControlMachine=`hostname -s`
|
||
AuthType=auth/munge
|
||
SlurmUser=slurm
|
||
SlurmdUser=root
|
||
SlurmctldPort=6817
|
||
SlurmdPort=6818
|
||
|
||
StateSaveLocation=/var/spool/slurm/ctld
|
||
SlurmdSpoolDir=/var/spool/slurm/d
|
||
SlurmctldPidFile=/var/run/slurmctld.pid
|
||
SlurmdPidFile=/var/run/slurmd.pid
|
||
ProctrackType=proctrack/linuxproc
|
||
SchedulerType=sched/backfill
|
||
SelectType=select/cons_tres
|
||
SelectTypeParameters=CR_Core
|
||
|
||
AccountingStorageType=accounting_storage/slurmdbd
|
||
AccountingStorageHost=localhost
|
||
AccountingStoragePort=6819
|
||
MailProg=/bin/true
|
||
|
||
${nodeinfo} State=UNKNOWN
|
||
PartitionName=control Nodes=cluster Default=YES MaxTime=INFINITE State=UP
|
||
EOSLURMCTLD
|
||
touch $slurmctld_flags
|
||
|
||
if [[ ! -f $slurmctld_flags ]]; then
|
||
systemctl restart slurmctld &>/dev/null
|
||
systemctl is-enabled slurmctld &>/dev/null || systemctl enable slurmctld &>/dev/null
|
||
fi
|
||
|
||
echo "$SSEP"
|
||
/usr/bin/sacctmgr show clusters format=cluster,controlhost,controlport,qos
|
||
#/usr/bin/sacctmgr show clusters format=cluster,controlhost,controlport --noheader
|
||
echo "$SSEP"
|
||
/usr/bin/sinfo -N -l | tail -n +2
|
||
echo "$SSEP"
|
||
}
|
||
|
||
# ====================== 核心函数:节点解析 ======================
|
||
load_nodes_from_file() {
|
||
local nodes_file="./nodes"
|
||
|
||
# 如果文件不存在,使用默认内置节点
|
||
if [[ ! -f "${nodes_file}" ]]; then
|
||
log_debug "节点文件 ${nodes_file} 不存在,使用内置默认节点"
|
||
return
|
||
fi
|
||
|
||
# 读取文件,去空行、去注释、去重、去前后空格
|
||
mapfile -t lines < "${nodes_file}"
|
||
DEFAULT_NODES=()
|
||
for line in "${lines[@]}"; do
|
||
# 跳过空行/空白行
|
||
[ -z "${line// /}" ] && continue
|
||
|
||
# 把一行切成数组(按空格分割)
|
||
cols=($line)
|
||
|
||
node="${cols[0]}" # 节点名称(必须)
|
||
port="${cols[1]:-22}" # 端口不存在 -> 默认 22
|
||
addr="${cols[2]:-}" # IP 不存在 -> 默认空字符串
|
||
|
||
# 按空格分割成数组
|
||
GLOBAL_PORT["$node"]=${port}
|
||
|
||
# 去掉空格 + 跳过空行/注释行
|
||
clean_line=$(echo "${node}" | xargs)
|
||
[[ -z "${clean_line}" || "${clean_line}" =~ ^# ]] && continue
|
||
DEFAULT_NODES+=("${node}")
|
||
done
|
||
|
||
log_info "成功从 nodes 文件加载节点:${#DEFAULT_NODES[@]} 个"
|
||
log_debug "nodes 文件节点列表:${DEFAULT_NODES[*]}"
|
||
}
|
||
|
||
parse_nodes() {
|
||
local node_val="$1"
|
||
GLOBAL_NODES=()
|
||
|
||
if [[ "$node_val" == "all" ]]; then
|
||
GLOBAL_NODES=("${DEFAULT_NODES[@]}")
|
||
else
|
||
IFS=',' read -ra tmp <<< "$node_val"
|
||
GLOBAL_NODES=("${tmp[@]}")
|
||
fi
|
||
|
||
GLOBAL_OPTS[nodes]="$node_val"
|
||
}
|
||
|
||
# 检查节点状态(online/offline/auth/pass)
|
||
check_node_status() {
|
||
local node="$1"
|
||
port=${GLOBAL_PORT[$node]}
|
||
|
||
# 检查网络连通性
|
||
if ! ping -c 1 -W 1 "$node" &>/dev/null; then
|
||
echo "offline"
|
||
return
|
||
fi
|
||
|
||
# 检查是否是本机
|
||
localhost=$(hostname -s)
|
||
if [[ "$node" == "localhost" || "$node" == $localhost ]]; then
|
||
echo "localhost"
|
||
return
|
||
fi
|
||
|
||
# 检查SSH免密登录
|
||
if ssh -o ConnectTimeout=1 -o BatchMode=yes -o StrictHostKeyChecking=accept-new -p $port "$node" true &>/dev/null; then
|
||
echo "online,auth"
|
||
else
|
||
echo "online,pass"
|
||
fi
|
||
}
|
||
|
||
list_node() {
|
||
echo "$SSEP"
|
||
printf "${G}%-12s %-15s %-8s %s${NC}\n" "Nodename" "Status" "Port" "Days"
|
||
echo "$SSEP"
|
||
for node in "${GLOBAL_NODES[@]}"; do
|
||
status=$(check_node_status "$node")
|
||
port=${GLOBAL_PORT[$node]}
|
||
|
||
if [[ "$status" == "offline" ]]; then
|
||
uptime_days="---"
|
||
else
|
||
uptime_days=$(ssh -o ConnectTimeout=1 -o BatchMode=yes -p$port "$node" "uptime \
|
||
| awk -F'up|days' '{print \$2}' | tr -d ',' | xargs" 2>/dev/null || echo "---")
|
||
fi
|
||
|
||
printf "%-12s %-15s %-8s %s\n" "$node" "$status" "$port" "$uptime_days"
|
||
done
|
||
echo "$SSEP"
|
||
}
|
||
|
||
run_cmd() {
|
||
local node="$1"
|
||
local cmds="$2"
|
||
local output=""
|
||
local exit_code=0
|
||
|
||
# 获取本机主机名(自动识别管理节点)
|
||
local localhost=$(hostname -s)
|
||
if [[ "$node" == "$localhost" || "$node" == "localhost" ]]; then
|
||
# ====================== 本机:直接执行 ======================
|
||
output=$(eval "$cmds" 2>&1)
|
||
exit_code=$?
|
||
else
|
||
# ====================== 远程:SSH 执行 ======================
|
||
output=$(ssh -o ConnectTimeout=1 -o BatchMode=yes "$node" "$cmds" 2>&1)
|
||
exit_code=$?
|
||
fi
|
||
|
||
|
||
if [[ $exit_code -eq 0 ]]; then
|
||
info "执行成功: ${node}"
|
||
else
|
||
warn "执行失败: ${node} (${R}Code${NC}: ${R}$exit_code${NC})"
|
||
fi
|
||
echo "$DSEP"
|
||
|
||
echo "$output"
|
||
}
|
||
|
||
|
||
|
||
# ====================== 核心函数: 同步文件路径处理 ======================
|
||
sync_file_path() {
|
||
local src_file="$1"
|
||
|
||
# ======================== 核心规则 ========================
|
||
# 1. 如果源是 绝对路径(/开头),直接返回原路径(系统目录)
|
||
if [[ "$src_file" == /* ]]; then
|
||
echo "$src_file"
|
||
return
|
||
fi
|
||
|
||
# 2. 如果源是 相对路径(linux/alls/* 或 linux/节点/*)
|
||
# 自动去掉 linux/xxx/ 前缀,剩余部分自动变成 绝对路径(前面加 /)
|
||
# 支持:etc usr opt var home root 任意目录,无限扩展
|
||
# ==================================================
|
||
local rel_path
|
||
rel_path=$(echo "$src_file" | sed -E 's#^linux/(alls|[-_a-zA-Z0-9]+)/##')
|
||
|
||
# 拼接为绝对路径
|
||
echo "/$rel_path"
|
||
}
|
||
|
||
sync_file_all() {
|
||
load_nodes "$NODE_ARG"
|
||
|
||
# ===================== 美化打印节点列表(固定缩进 + 自动换行) =====================
|
||
local indent=" " # 11个空格
|
||
local line=""
|
||
local count=0
|
||
local max_per_line=5 # 每行显示几个节点
|
||
|
||
info "开始同步所有节点文件,节点列表:"
|
||
for node in "${NODES[@]}"; do
|
||
line+="$node "
|
||
count=$((count + 1))
|
||
|
||
if [[ $count -ge $max_per_line ]]; then
|
||
echo -e "${indent}${line}"
|
||
line=""
|
||
count=0
|
||
fi
|
||
done
|
||
|
||
# 打印剩余不足一行的节点
|
||
if [[ -n "$line" ]]; then
|
||
echo -e "${indent}${line}"
|
||
fi
|
||
# ================================================================================
|
||
for node_dir in linux/*; do
|
||
[[ -d "$node_dir" ]] || continue
|
||
|
||
for sub_dir in "$node_dir"/*; do
|
||
[[ -d "$sub_dir" ]] || continue
|
||
|
||
info "处理源目录:$sub_dir"
|
||
|
||
for node in "${NODES[@]}"; do
|
||
if [[ "$node_dir" == "linux/alls" || "$node_dir" == "linux/$node" ]]; then
|
||
target=$(sync_file_path "$sub_dir")
|
||
info "同步 ${Y}$sub_dir${NC} -> ${R}$node${NC}:${Y}$target${NC}"
|
||
rsync -avzAP --info=progress2 --exclude='.*' "$sub_dir"/ "$node":"$target"/
|
||
fi
|
||
done
|
||
done
|
||
done
|
||
|
||
info "同步完成!"
|
||
}
|
||
|
||
sync_file_single() {
|
||
local src_file="$1"
|
||
[[ -z "$src_file" ]] && error "请使用 -f 指定文件路径"
|
||
[[ -e "$src_file" ]] || error "文件不存在:$src_file"
|
||
|
||
load_nodes "$NODE_ARG"
|
||
|
||
local indent=" " # 11个空格
|
||
local line=""
|
||
local count=0
|
||
local max_per_line=5 # 每行显示几个节点
|
||
|
||
info "开始同步所有节点文件,节点列表:"
|
||
for node in "${NODES[@]}"; do
|
||
line+="$node "
|
||
count=$((count + 1))
|
||
|
||
if [[ $count -ge $max_per_line ]]; then
|
||
echo -e "${indent}${line}"
|
||
line=""
|
||
count=0
|
||
fi
|
||
done
|
||
|
||
# 打印剩余不足一行的节点
|
||
if [[ -n "$line" ]]; then
|
||
echo -e "${indent}${line}"
|
||
fi
|
||
|
||
target=$(sync_file_path "$src_file")
|
||
for node in "${NODES[@]}"; do
|
||
info "同步 ${Y}$src_file${NC} -> ${R}$node${NC}:${Y}$target${NC}"
|
||
rsync -avzAP --info=progress2 "$src_file" "$node":"$target"
|
||
done
|
||
info "同步完成!"
|
||
}
|
||
# ====================== 帮助文档 ======================
|
||
show_help() {
|
||
cat <<EOF
|
||
SunHPC 命令行工具
|
||
|
||
Usage: sunhpc <COMMAND> [OPTIONS]
|
||
|
||
公共参数:
|
||
-v, --verbose 详细输出
|
||
-d, --debug 调试模式
|
||
-h, --help 帮助信息
|
||
-n, --nodes <list> 指定节点(all 或 node1,node2,node3)
|
||
|
||
支持命令:
|
||
sunhpc pxe dhcp 部署dhcp服务
|
||
sunhpc pxe tftp 部署tftp服务
|
||
sunhpc pxe http 部署http服务
|
||
sunhpc pxe dnsmasq 部署pxe服务
|
||
sunhpc exec 批量执行命令
|
||
sunhpc sync file 执行同步任务
|
||
sunhpc list node 查看节点列表
|
||
sunhpc show vimrc 显示Vimrc变量
|
||
sunhpc show system 显示系统信息
|
||
sunhpc serv slurm 构建 slurm 服务
|
||
sunhpc serv autofs 构建 autofs 服务
|
||
sunhpc create syncfile 显示系统信息
|
||
|
||
Report bugs to: <support@sunhpc.com>
|
||
Htool URL: https://gitea.sunhpc.com
|
||
Gitea URL: https://gitea.sunhpc.com/htool.git
|
||
EOF
|
||
}
|
||
|
||
show_help_sync() {
|
||
cat <<EOF
|
||
SunHPC 命令行工具
|
||
|
||
Usage: sunhpc <COMMAND> [OPTIONS]
|
||
|
||
公共参数:
|
||
-v, --verbose 详细输出
|
||
-d, --debug 调试模式
|
||
-h, --help 帮助信息
|
||
-n, --nodes <list> 指定节点(all 或 node1,node2,node3)
|
||
|
||
命令示例:
|
||
sunhpc sync file -f/--file <filepath> 同步单个文件
|
||
sunhpc sync file -a/--all 同步所有文件
|
||
|
||
Report bugs to: <support@sunhpc.com>
|
||
Htool URL: https://gitea.sunhpc.com
|
||
Gitea URL: https://gitea.sunhpc.com/htool.git
|
||
EOF
|
||
}
|
||
|
||
show_help_slurm() {
|
||
cat <<EOF
|
||
SunHPC 命令行工具
|
||
|
||
Usage: sunhpc <COMMAND> [OPTIONS]
|
||
|
||
公共参数:
|
||
-v, --verbose 详细输出
|
||
-d, --debug 调试模式
|
||
-h, --help 帮助信息
|
||
-n, --nodes <list> 指定节点(all 或 node1,node2,node3)
|
||
|
||
命令示例:
|
||
sunhpc serv slurm -b/--build 构建Slurm安装包
|
||
sunhpc serv slurm -i/--install -m/master 安装Slurm到管理节点
|
||
sunhpc serv slurm -i/--install -c/compute 安装Slurm到计算节点
|
||
|
||
Report bugs to: <support@sunhpc.com>
|
||
Htool URL: https://gitea.sunhpc.com
|
||
Gitea URL: https://gitea.sunhpc.com/htool.git
|
||
EOF
|
||
}
|
||
|
||
# ====================== Verbose/Debug 输出工具 ======================
|
||
log_info() {
|
||
if [[ -n "${GLOBAL_OPTS[v]}" || -n "${GLOBAL_OPTS[verbose]}" ]]; then
|
||
echo -e "\033[32m[I]\033[0m $1"
|
||
fi
|
||
}
|
||
|
||
log_debug() {
|
||
if [[ -n "${GLOBAL_OPTS[d]}" || -n "${GLOBAL_OPTS[debug]}" ]]; then
|
||
echo -e "\033[33m[D]\033[0m $1"
|
||
fi
|
||
}
|
||
|
||
# ====================== 子命令执行 ======================
|
||
run_pxe_dhcp() {
|
||
log_debug "--------- run_pxe_dhcp----------------"
|
||
log_debug "当前节点:${GLOBAL_NODES[*]}"
|
||
log_debug "公共参数:${PUBLIC_ARGS[*]}"
|
||
log_debug "私有参数:${PRIVATE_ARGS[*]}"
|
||
|
||
# -n 非空True,-z 为空True
|
||
[[ -n ${GLOBAL_OPTS[interface]} ]] && pxe_dhcp_build || \
|
||
warn "请指定DHCP监听接口命名, 例如: sunhpc pxe dhcp -i eth0"
|
||
}
|
||
run_pxe_tftp() {
|
||
log_debug "--------- run_pxe_tftp----------------"
|
||
log_debug "当前节点:${GLOBAL_NODES[*]}"
|
||
log_debug "公共参数:${PUBLIC_ARGS[*]}"
|
||
log_debug "私有参数:${PRIVATE_ARGS[*]}"
|
||
pxe_tftp_build
|
||
}
|
||
run_pxe_http() {
|
||
log_debug "--------- run_pxe_http----------------"
|
||
log_debug "当前节点:${GLOBAL_NODES[*]}"
|
||
log_debug "公共参数:${PUBLIC_ARGS[*]}"
|
||
log_debug "私有参数:${PRIVATE_ARGS[*]}"
|
||
pxe_http_build
|
||
}
|
||
|
||
run_init() {
|
||
|
||
[[ -n "${GLOBAL_OPTS[interface]}" ]] && iface="${GLOBAL_OPTS[interface]}" || \
|
||
error "必须指定一个网络接口,例如: -i/--interface eth1"
|
||
|
||
[[ -n "${GLOBAL_OPTS[html]-}" ]] && html="${GLOBAL_OPTS[html]}" || \
|
||
error "必须指定 html 路径,例如: --html /var/www/html"
|
||
|
||
ipaddr=${global_env[${iface}_ip]}
|
||
netcidr=${global_env[${iface}_mask]}
|
||
netmask=$(cidr_to_netmask 24)
|
||
subnet=$(get_subnet_address $ipaddr $netmask)
|
||
|
||
IFS=. read -r i1 i2 i3 i4 <<< "$ipaddr"
|
||
start_ip="$i1.$i2.$i3.10"
|
||
ender_ip="$i1.$i2.$i3.90"
|
||
|
||
|
||
tmpdir=$(find -L $html -name ".treeinfo")
|
||
tmpdir=$(dirname ${tmpdir#$html})
|
||
|
||
echo "iface---------> $iface"
|
||
echo "addr----------> $ipaddr"
|
||
echo "startip-------> $start_ip"
|
||
echo "enderip-------> $ender_ip"
|
||
echo "htmlroot------> $html"
|
||
echo "reporoot------> $tmpdir"
|
||
exit 1
|
||
|
||
{
|
||
echo "interface $iface"
|
||
echo "server $ipaddr"
|
||
echo "range $start_ip,$ender_ip"
|
||
echo "htmlroot $html"
|
||
echo "reporoot $tmpdir"
|
||
} > ${config_file}
|
||
#gen_config_file
|
||
}
|
||
|
||
run_exec() {
|
||
log_debug "--------- run_exec----------------"
|
||
log_debug "当前节点:${GLOBAL_NODES[*]}"
|
||
log_debug "公共参数:${PUBLIC_ARGS[*]}"
|
||
log_debug "私有参数:${PRIVATE_ARGS[*]}"
|
||
|
||
local cmds=$1
|
||
[[ -z "$cmds" ]] && error "请指定要执行的命令,例如:sunhpc exec \"df -h\""
|
||
|
||
echo "$DSEP"
|
||
info "执行命令: $cmds"
|
||
|
||
localhost=$(hostname -s)
|
||
for node in "${GLOBAL_NODES[@]}"; do
|
||
if [[ "$node" != "$localhost" || "$node" != "localhost" ]]; then
|
||
status=$(check_node_status "$node")
|
||
if [[ "$status" == "offline" ]]; then
|
||
warn "$node 节点离线,跳过执行"
|
||
continue
|
||
fi
|
||
if [[ "$status" == "online,pass" ]]; then
|
||
warn "$node 需要密码登录,跳过执行"
|
||
continue
|
||
fi
|
||
fi
|
||
|
||
# ========== 核心:自动本机/远程执行 ==========
|
||
run_cmd "$node" "$cmds"
|
||
|
||
done
|
||
echo "$SSEP"
|
||
info "命令执行完成!"
|
||
echo "$SSEP"
|
||
}
|
||
|
||
run_sync() {
|
||
log_debug "--------- run_sync----------------"
|
||
log_debug "当前节点:${GLOBAL_NODES[*]}"
|
||
log_debug "公共参数:${PUBLIC_ARGS[*]}"
|
||
log_debug "私有参数:${PRIVATE_ARGS[*]}"
|
||
|
||
case "${PRIVATE_ARGS[@]}" in
|
||
-a|--all) sync_file_all ;;
|
||
-f|--file) sync_file_single ;;
|
||
*) show_help_sync ;;
|
||
esac
|
||
|
||
}
|
||
|
||
run_list_node() {
|
||
log_debug "--------- run_list_node----------------"
|
||
log_debug "当前节点:${GLOBAL_NODES[*]}"
|
||
log_debug "公共参数:${PUBLIC_ARGS[*]}"
|
||
log_debug "私有参数:${PRIVATE_ARGS[*]}"
|
||
|
||
log_info "执行 list node"
|
||
log_info "当前节点列表:${GLOBAL_NODES[*]}"
|
||
list_node
|
||
}
|
||
|
||
run_show_vimrc() {
|
||
cat <<-EOF
|
||
" 开启语法高亮
|
||
syntax enable
|
||
|
||
" 设置缩进为4个空格
|
||
set tabstop=4
|
||
set softtabstop=4
|
||
set shiftwidth=4
|
||
|
||
" 用空格替代Tab键(禁止输入真实Tab)
|
||
set expandtab
|
||
|
||
" 自动缩进(写代码更方便)
|
||
set autoindent
|
||
set smartindent
|
||
EOF
|
||
}
|
||
|
||
run_serv_slurm() {
|
||
log_debug "--------- run_serv_slurm------------------"
|
||
case "${PRIVATE_ARGS[@]}" in
|
||
-b|--build) slurm_build ;;
|
||
*) show_help_slurm ;;
|
||
esac
|
||
}
|
||
|
||
run_serv_autofs() {
|
||
log_debug "--------- run_serv_autofs----------------"
|
||
log_debug "当前节点:${GLOBAL_NODES[*]}"
|
||
log_debug "公共参数:${PUBLIC_ARGS[*]}"
|
||
log_debug "私有参数:${PRIVATE_ARGS[*]}"
|
||
|
||
log_info "执行 server autofs"
|
||
if [[ " ${PRIVATE_ARGS[@]} " =~ " --build " ]]; then
|
||
log_debug "检测到 --build 参数,开始构建..."
|
||
echo "✅ 正在构建 autofs 服务"
|
||
fi
|
||
}
|
||
|
||
create_sync_file() {
|
||
dirspath=(
|
||
"/etc/passwd"
|
||
"/etc/shadow"
|
||
"/etc/group"
|
||
"/etc/slurm"
|
||
"/etc/systemd/system"
|
||
"/usr/lib/systemd/system"
|
||
)
|
||
|
||
local base="${1:-.}"
|
||
local name="${2:-linux}"
|
||
local all_dir="${base}/${name}/all"
|
||
|
||
mkdir -p "${all_dir}" || return 1
|
||
for d in "${dirspath}"; do
|
||
if [[ -f $d ]]; then
|
||
src_dir=$(dirname "$d")
|
||
dst_dir="${all_dir}/$src_dir"
|
||
mkdir -p "${dst_dir}"
|
||
/usr/bin/cp $d ${dst_dir}
|
||
else
|
||
mkdir -p "${all_dir}/${d}"
|
||
fi
|
||
done
|
||
|
||
for n in ${GLOBAL_NODES[@]}; do
|
||
mkdir -p "${base}/${name}/${n}"
|
||
done
|
||
|
||
}
|
||
|
||
# ====================== 命令路由 ======================
|
||
dispatch() {
|
||
# 优先显示帮助
|
||
if [[ -n "${GLOBAL_OPTS[h]}" || -n "${GLOBAL_OPTS[help]}" || ${#CMD_STACK[@]} -eq 0 ]]; then
|
||
show_help
|
||
exit 0
|
||
fi
|
||
|
||
log_debug "$DSEP"
|
||
log_debug "解析子命令:${CMD_STACK[*]}"
|
||
log_debug "专属参数 :${PRIVATE_ARGS[*]}"
|
||
|
||
case "${CMD_STACK[0]}" in
|
||
pxe)
|
||
case "${CMD_STACK[1]}" in
|
||
dhcp) run_pxe_dhcp ;;
|
||
tftp) run_pxe_tftp;;
|
||
http) run_pxe_http;;
|
||
dnsmasq) pxe_dnsmasq_build;;
|
||
*) show_help;;
|
||
esac
|
||
;;
|
||
exec)
|
||
run_exec "${CMD_STACK[1]}"
|
||
;;
|
||
sync)
|
||
case "${CMD_STACK[1]}" in
|
||
file) run_sync ;;
|
||
*) show_help ;;
|
||
esac
|
||
;;
|
||
list)
|
||
case "${CMD_STACK[1]}" in
|
||
node) run_list_node ;;
|
||
*) show_help ;;
|
||
esac
|
||
;;
|
||
show)
|
||
case "${CMD_STACK[1]}" in
|
||
vimrc) run_show_vimrc ;;
|
||
system) show_system_summary ;;
|
||
*) show_help;;
|
||
esac
|
||
;;
|
||
serv)
|
||
case "${CMD_STACK[1]}" in
|
||
slurm) run_serv_slurm ;;
|
||
autofs) run_serv_autofs ;;
|
||
*) show_help;;
|
||
esac
|
||
;;
|
||
create)
|
||
case "${CMD_STACK[1]}" in
|
||
syncfile) create_sync_file ;;
|
||
*) show_help ;;
|
||
esac
|
||
;;
|
||
*) show_help ;;
|
||
esac
|
||
}
|
||
|
||
# ====================== 核心函数:参数解析 ======================
|
||
parse_args() {
|
||
while [[ $# -gt 0 ]]; do
|
||
local arg="$1"
|
||
case "$arg" in
|
||
# 公共短参数:-v -d -h -n
|
||
-v) GLOBAL_OPTS[v]=1; GLOBAL_OPTS[verbose]=1; PUBLIC_ARGS+=("$arg"); shift ;;
|
||
-d) GLOBAL_OPTS[d]=1; GLOBAL_OPTS[debug]=1; PUBLIC_ARGS+=("$arg"); shift ;;
|
||
-h) GLOBAL_OPTS[h]=1; GLOBAL_OPTS[help]=1; PUBLIC_ARGS+=("$arg"); shift ;;
|
||
-f) GLOBAL_OPTS[file]=$2; PUBLIC_ARGS+=("$arg" "$2"); shift 2 ;;
|
||
-i) GLOBAL_OPTS[interface]=$2; PUBLIC_ARGS+=("$arg" "$2"); shift 2 ;;
|
||
-n) parse_nodes "$2"; PUBLIC_ARGS+=("$arg" "$2"); shift 2 ;;
|
||
|
||
# 公共长参数:--verbose --debug --help --nodes
|
||
--verbose) GLOBAL_OPTS[verbose]=1; GLOBAL_OPTS[v]=1; PUBLIC_ARGS+=("$arg"); shift ;;
|
||
--debug) GLOBAL_OPTS[debug]=1; GLOBAL_OPTS[d]=1; PUBLIC_ARGS+=("$arg"); shift ;;
|
||
--help) GLOBAL_OPTS[help]=1; GLOBAL_OPTS[h]=1; PUBLIC_ARGS+=("$arg"); shift ;;
|
||
--file) GLOBAL_OPTS[file]=$2; PUBLIC_ARGS+=("$arg" "$2"); shift 2 ;;
|
||
--html) GLOBAL_OPTS[html]=$2; PUBLIC_ARGS+=("$arg" "$2"); shift 2 ;;
|
||
--interface) GLOBAL_OPTS[interface]=$2; PUBLIC_ARGS+=("$arg" "$2"); shift 2 ;;
|
||
--nodes) parse_nodes "$2"; PUBLIC_ARGS+=("$arg" "$2"); shift 2 ;;
|
||
|
||
# -- 专属短参数(子命令私有)
|
||
-*) PRIVATE_ARGS+=("$arg"); shift ;;
|
||
# -- 专属长参数(子命令私有)
|
||
--*) PRIVATE_ARGS+=("$arg"); shift ;;
|
||
|
||
# 子命令(无 - / --)
|
||
*) CMD_STACK+=("$arg"); shift ;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
# ====================== 主入口 ======================
|
||
main() {
|
||
# 配置文件
|
||
config_file="htool.conf"
|
||
|
||
# load system env
|
||
load_os_envs
|
||
|
||
# 启动时先加载 nodes 文件
|
||
load_nodes_from_file
|
||
|
||
# 默认使用文件中的节点
|
||
GLOBAL_NODES=("${DEFAULT_NODES[@]}")
|
||
|
||
parse_args "$@"
|
||
|
||
# 获取基础信息存储变量
|
||
if [[ -e $config_file ]]; then
|
||
load_conf_envs
|
||
else
|
||
run_init
|
||
fi
|
||
|
||
dispatch
|
||
}
|
||
|
||
main "$@"
|