#!/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 <&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 "" echo " Options Indexes FollowSymLinks" echo " AllowOverride None" echo " Require all granted" echo "" echo " EnableSendfile on" echo "" } > ${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 </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 < [OPTIONS] 公共参数: -v, --verbose 详细输出 -d, --debug 调试模式 -h, --help 帮助信息 -n, --nodes 指定节点(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: Htool URL: https://gitea.sunhpc.com Gitea URL: https://gitea.sunhpc.com/htool.git EOF } show_help_sync() { cat < [OPTIONS] 公共参数: -v, --verbose 详细输出 -d, --debug 调试模式 -h, --help 帮助信息 -n, --nodes 指定节点(all 或 node1,node2,node3) 命令示例: sunhpc sync file -f/--file 同步单个文件 sunhpc sync file -a/--all 同步所有文件 Report bugs to: Htool URL: https://gitea.sunhpc.com Gitea URL: https://gitea.sunhpc.com/htool.git EOF } show_help_slurm() { cat < [OPTIONS] 公共参数: -v, --verbose 详细输出 -d, --debug 调试模式 -h, --help 帮助信息 -n, --nodes 指定节点(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: 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 "$@"