1503 lines
46 KiB
Bash
Executable File
1503 lines
46 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
|
||
declare -A global_treeinfo
|
||
|
||
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
|
||
}
|
||
|
||
load_conf_envs() {
|
||
|
||
while read -r key val; do
|
||
# 跳过空行、注释行
|
||
[[ -z "$key" || "$key" == \#* ]] && continue
|
||
|
||
global_env["$key"]="$val"
|
||
done < "$config_file"
|
||
}
|
||
|
||
parse_treeinfo() {
|
||
local treeinfo_file="${1:-.treeinfo}"
|
||
local current_section=""
|
||
|
||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||
# 跳过空行和注释行
|
||
[[ -z "$line" || "$line" =~ ^[[:space:]]*\; ]] && continue
|
||
|
||
# 匹配 section,例如 [general]
|
||
if [[ "$line" =~ ^\[([^]]+)\]$ ]]; then
|
||
current_section="${BASH_REMATCH[1]}"
|
||
continue
|
||
fi
|
||
|
||
# 匹配 key = value
|
||
if [[ "$line" =~ ^[[:space:]]*([^=]+)[[:space:]]*=[[:space:]]*(.*)$ ]]; then
|
||
local key="${BASH_REMATCH[1]}"
|
||
local value="${BASH_REMATCH[2]}"
|
||
|
||
key="${key#"${key%%[![:space:]]*}"}"
|
||
key="${key%"${key##*[![:space:]]}"}"
|
||
|
||
# 去除值两端和中间的所有空格
|
||
value="${value#"${value%%[![:space:]]*}"}"
|
||
value="${value%"${value##*[![:space:]]}"}"
|
||
value=$(echo "$value" | tr -d ' ' | tr '[:upper:]' '[:lower:]')
|
||
|
||
# 存储到关联数组,使用 "section.key" 作为键名
|
||
global_treeinfo["${current_section}.${key}"]="$value"
|
||
fi
|
||
done < "$treeinfo_file"
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
getArgs() {
|
||
# getArgs interface iface e "msg"
|
||
local key="$1"
|
||
local result="$2"
|
||
local level="${3:-w}" # 默认级别为 warn
|
||
local msg="${4:-}"
|
||
local value=""
|
||
|
||
# 优先级1:检查 GLOBAL_OPTS(命令行参数)
|
||
if [[ -n "${GLOBAL_OPTS[$key]}" ]]; then
|
||
value="${GLOBAL_OPTS[$key]}"
|
||
# 优先级2:检查 global_env(配置文件)
|
||
elif [[ -n "${global_env[$key]}" ]]; then
|
||
value="${global_env[$key]}"
|
||
fi
|
||
|
||
# 检查 GLOBAL_OPTS 数组中是否存在该键
|
||
if [[ -n "$value" ]]; then
|
||
printf -v "$result" "%s" "$value"
|
||
return 0
|
||
else
|
||
# 根据不同的级别处理错误
|
||
case "$level" in
|
||
e)
|
||
[[ -n "$msg" ]] && error "$msg" || error "必须指定 --$key 参数"
|
||
;;
|
||
w)
|
||
[[ -n "$msg" ]] && warn "$msg" || warn "未指定 --$key 参数,将使用默认值或跳过"
|
||
;;
|
||
i)
|
||
[[ -n "$msg" ]] && info "$msg" || info "未指定 --$key 参数"
|
||
;;
|
||
*)
|
||
[[ -n "$msg" ]] && info "$msg" || info "未指定 --$key 参数"
|
||
;;
|
||
esac
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# ============================================================
|
||
# 辅助函数:打印所有环境变量(调试用)
|
||
# ============================================================
|
||
print_global_env() {
|
||
for key in "${!global_env[@]}"; do
|
||
printf "%-20s = %s\n" "$key" "${global_env[$key]}"
|
||
done
|
||
echo "======================================"
|
||
}
|
||
|
||
# ============================================================
|
||
# 辅助函数:快速获取系统信息摘要
|
||
# ============================================================
|
||
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 ====================
|
||
build_httpd_srv() {
|
||
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 /linux /var/www/html/linux'
|
||
echo ''
|
||
echo 'UseCanonicalName Off'
|
||
echo "ServerName `hostname -f`"
|
||
echo ''
|
||
echo '<Directory "/var/www/html/linux">'
|
||
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
|
||
|
||
|
||
if systemctl is-active httpd &>/dev/null ; then
|
||
info "HTTP 服务部署成功"
|
||
info "HTTP 服务端口: 80,443"
|
||
else
|
||
warn "HTTP 服务部署失败,请查看服务信息 systemctl status httpd.service"
|
||
fi
|
||
}
|
||
|
||
build_dnsmasq_srv() {
|
||
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"
|
||
|
||
getArgs interface iface e "必须指定一个网络接口,例如: -i/--interface eth1"
|
||
getArgs server ipaddr e "Check the configuration file $config_file: server <ipaddr>"
|
||
getArgs tftpboot tftpboot e "Check the configuration file $config_file: tftpboot <fullpath>"
|
||
getArgs startip start_ip e "Check the configuration file $config_file: startip <ipaddr>"
|
||
getArgs endip ender_ip e "Check the configuration file $config_file: endip <ipaddr>"
|
||
getArgs dns dnsip e "Check the configuration file $config_file: dns <dnsaddr>"
|
||
|
||
getArgs bios biosboot e "Check the configuration file $config_file: biosboot <fullpath>"
|
||
getArgs uefi uefiboot e "Check the configuration file $config_file: uefiboot <fullpath>"
|
||
getArgs ipxeboot ipxeboot e "Check the configuration file $config_file: ipxeboot <url>"
|
||
|
||
|
||
biosboot="${biosboot#$tftpboot/}"
|
||
uefiboot="${uefiboot#$tftpboot/}"
|
||
dnsmasq_conf="/etc/dnsmasq.d/sunhpc.conf"
|
||
|
||
{
|
||
echo "interface=$iface"
|
||
echo "bind-interfaces"
|
||
echo ""
|
||
echo "enable-tftp"
|
||
echo "tftp-root=${tftpboot}"
|
||
echo ""
|
||
echo "# Gateway:3, DNS:6"
|
||
echo "dhcp-range=$start_ip,$ender_ip,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,$biosboot"
|
||
echo "dhcp-boot=tag:uefi,$uefiboot"
|
||
echo "dhcp-boot=tag:ipxe,$ipxeboot"
|
||
} > ${dnsmasq_conf}
|
||
|
||
}
|
||
# ====================== server functions ========================
|
||
build_slurm_srv() {
|
||
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
|
||
}
|
||
|
||
show_nodes() {
|
||
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 "同步完成!"
|
||
}
|
||
|
||
# rsync 同步函数,支持 ISO 文件和普通目录
|
||
# 用法: rsync_sync_file <source> <destination> [rsync_options]
|
||
# 参数:
|
||
# source - 源路径(文件或目录,支持 ISO 文件)
|
||
# destination - 目标路径
|
||
# rsync_options - 可选,额外的 rsync 参数
|
||
# 返回:
|
||
# 0 - 成功
|
||
# 1 - 失败
|
||
rsync_sync_file() {
|
||
local source="$1"
|
||
local destination="$2"
|
||
local rsync_opts="${3:--ah --info=progress2}" # 默认 rsync 选项
|
||
local temp_mount=""
|
||
local is_iso=false
|
||
local exit_code=0
|
||
|
||
# 检查参数
|
||
if [[ -z "$source" ]] || [[ -z "$destination" ]]; then
|
||
warn "必须指定源路径和目标路径" >&2
|
||
warn "用法: rsync_sync_file <source> <destination> [rsync_options]" >&2
|
||
return 1
|
||
fi
|
||
|
||
# 检查源路径是否存在
|
||
if [[ ! -e "$source" ]]; then
|
||
warn "源路径不存在: $source" >&2
|
||
return 1
|
||
fi
|
||
|
||
# 检查是否为 ISO 文件
|
||
if [[ -f "$source" && ( "$source" =~ \.(iso|ISO)$ ) ]]; then
|
||
is_iso=true
|
||
info "检测到 ISO 文件: $source"
|
||
|
||
# 创建临时挂载点
|
||
temp_mount=$(mktemp -d "/tmp/iso_mount_XXXXXX")
|
||
info "创建临时挂载点: $temp_mount"
|
||
|
||
# 挂载 ISO 文件
|
||
if mount -o loop "$source" "$temp_mount" 2>/dev/null; then
|
||
info "成功挂载 ISO 文件到: $temp_mount"
|
||
source="$temp_mount"
|
||
else
|
||
warn "无法挂载 ISO 文件,可能需要 root 权限" >&2
|
||
rmdir "$temp_mount" 2>/dev/null
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
# 确保目标目录存在
|
||
# 提取目标目录(如果是文件,取目录部分)
|
||
local dest_dir
|
||
if [[ "$destination" == */ ]]; then
|
||
dest_dir="$destination" # 目标 / 结尾, 视为目录
|
||
else
|
||
dest_dir="$(dirname "$destination")" # 目标是文件, 取目录部分
|
||
fi
|
||
|
||
if [[ ! -d "$dest_dir" ]]; then
|
||
info "创建目标目录: $dest_dir"
|
||
mkdir -p "$dest_dir" || {
|
||
warn "无法创建目标目录: $dest_dir" >&2
|
||
exit_code=1
|
||
cleanup
|
||
}
|
||
fi
|
||
|
||
# 确保源路径以斜杠结尾(rsync 目录同步规范)
|
||
local rsync_source="$source"
|
||
if [[ -d "$source" ]]; then
|
||
# 如果是目录,确保以 / 结尾,这样同步目录内容而不是目录本身
|
||
rsync_source="${source}/"
|
||
fi
|
||
|
||
# 执行 rsync 同步
|
||
info "开始同步数据..."
|
||
info " 源路径: $source"
|
||
info " 目标路径: $destination"
|
||
info " rsync 选项: $rsync_opts"
|
||
|
||
rsync $rsync_opts "$rsync_source" "$destination"
|
||
exit_code=$?
|
||
|
||
if [[ $exit_code -eq 0 ]]; then
|
||
info "同步成功完成"
|
||
else
|
||
warn "rsync 同步失败,退出码: $exit_code" >&2
|
||
fi
|
||
|
||
# 清理:卸载临时挂载点
|
||
cleanup() {
|
||
if [[ "$is_iso" == "true" ]] && [[ -n "$temp_mount" ]]; then
|
||
info "清理临时挂载点..."
|
||
umount "$temp_mount" 2>/dev/null || {
|
||
warn "无法卸载 $temp_mount,可能需要手动清理" >&2
|
||
}
|
||
rmdir "$temp_mount" 2>/dev/null
|
||
info "已删除临时挂载点: $temp_mount"
|
||
fi
|
||
|
||
return $exit_code
|
||
}
|
||
}
|
||
# ====================== 帮助文档 ======================
|
||
show_help() {
|
||
cat <<EOF
|
||
SunHPC command tools
|
||
|
||
Usage: sunhpc <COMMAND> [OPTIONS]
|
||
|
||
Public Args:
|
||
-v, --verbose Verbose output
|
||
-d, --debug Debug Mode
|
||
-h, --help Display the help infomation
|
||
-n, --nodes <list> Specified node (all or node1,node2,node3)
|
||
|
||
Support commands:
|
||
sunhpc exec Batch execute command
|
||
sunhpc init Execute the init op
|
||
sunhpc sync file Run the synchronize file command
|
||
|
||
sunhpc show nodes Display nodes list
|
||
sunhpc show vimrc Dispaly Vimrc config
|
||
sunhpc show system Display the system infomation
|
||
|
||
sunhpc build httpd Build httpd server
|
||
sunhpc build slurm Build slurm server
|
||
sunhpc build autofs Build autofs server
|
||
sunhpc build dnsmasq Build dnsmasq server
|
||
sunhpc create syncfile Display the help infomation
|
||
|
||
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 command tools
|
||
|
||
Usage: sunhpc <COMMAND> [OPTIONS]
|
||
|
||
Public Args:
|
||
-v, --verbose Verbose output
|
||
-d, --debug Debug Mode
|
||
-h, --help Display the help infomation
|
||
-n, --nodes <list> Specified node (all or node1,node2,node3)
|
||
|
||
Command Example:
|
||
sunhpc sync file -f/--file <filepath> Sync a single file
|
||
sunhpc sync file -a/--all Sync all files
|
||
|
||
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 command tools
|
||
|
||
Usage: sunhpc <COMMAND> [OPTIONS]
|
||
|
||
Public Args:
|
||
-v, --verbose Verbose output
|
||
-d, --debug Debug Mode
|
||
-h, --help Display the help infomation
|
||
-n, --nodes <list> Specified node (all or node1,node2,node3)
|
||
|
||
Command Example:
|
||
sunhpc serv slurm -b/--build Build Slurm rpm packages
|
||
sunhpc serv slurm -i/--install -m/master Install Slurm to frontend
|
||
sunhpc serv slurm -i/--install -c/compute Install Slurm to computes
|
||
|
||
Report bugs to: <support@sunhpc.com>
|
||
Htool URL: https://gitea.sunhpc.com
|
||
Gitea URL: https://gitea.sunhpc.com/htool.git
|
||
EOF
|
||
}
|
||
|
||
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
|
||
}
|
||
# ====================== 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
|
||
}
|
||
|
||
# ====================== 子命令执行 ======================
|
||
init_run() {
|
||
|
||
info "初次使用此工具,需要在当前目录创建一个配置文件."
|
||
info " 1、手动创建配置文件,e.g., touch $config_file"
|
||
info " 2、自动创建配置文件,e.g., "
|
||
info " sunhpc init -i <interface> -m /mnt/cdrom"
|
||
echo ""
|
||
|
||
getArgs interface iface e "必须指定一个网络接口,例如: -i/--interface eth1"
|
||
getArgs mntroot mntroot e "必须指定 mnt 路径,例如: -m/--mntroot /mnt/cdrom"
|
||
|
||
treeinfo=$(find -L $mntroot -name ".treeinfo")
|
||
parse_treeinfo $treeinfo
|
||
|
||
for key in "${!global_treeinfo[@]}"; do
|
||
log_debug "key: '$key', Value: '${global_treeinfo[$key]}'";
|
||
done
|
||
|
||
[[ -z "$treeinfo" ]] && warn "提供的 $mntroot 目录为空,或是不被支持的系统."
|
||
|
||
nopkgs=()
|
||
pkgs=("ipxe-bootimgs")
|
||
filter_pkgs pkgs nopkgs
|
||
|
||
local cmd=("dnf -y install ${nopkgs[*]}")
|
||
[[ -z ${nopkgs[@]} ]] || run_with_progress "DNF Install ipxe tools..." "$cmd" "$log_file"
|
||
|
||
srv_root="/srv"
|
||
repos_root="$srv_root/repos"
|
||
|
||
kernel_path="${global_treeinfo[images-x86_64.kernel]}"
|
||
initrd_path="${global_treeinfo[images-x86_64.initrd]}"
|
||
|
||
sys_os_rels="${global_treeinfo[general.family]}" # RockyLinux
|
||
sys_os_vers="${global_treeinfo[general.version]}" # 9.7
|
||
url_os_path="$sys_os_rels/$sys_os_vers" # http://ip/ 后面系统路径 rockylinux/9.7
|
||
|
||
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"
|
||
|
||
mnt_dst="$repos_root/$url_os_path"
|
||
rsync_sync_file $mntroot $mnt_dst
|
||
|
||
lrepo_path="$repos_root/$sys_os_rels"
|
||
|
||
wwwroot="/var/www/html/linux"
|
||
[[ -e $wwwroot ]] || mkdir -p $wwwroot
|
||
|
||
ks_src="$PWD/pkgs/"
|
||
ks_dst="$srv_root/htool/pkgs/"
|
||
[[ -e $ks_src ]] && rsync_sync_file $ks_src $ks_dst
|
||
|
||
tftpboot="/srv/pxelinux/tftpboot"
|
||
|
||
efi_src="$mnt_dst/EFI/BOOT/BOOTX64.EFI"
|
||
[[ -e $efi_src ]] && rsync_sync_file $efi_src $tftpboot/bootx64.efi \
|
||
|| warn "UEFI Boot file no found: $efi_src"
|
||
|
||
ipxe_src="/usr/share/ipxe/undionly.kpxe"
|
||
[[ -e $ipxe_src ]] && rsync_sync_file $ipxe_src $tftpboot/boot/ \
|
||
|| warn "iPXE Boot file no found: $ipxe_tools"
|
||
|
||
ks_args="net.ifnames=0 biosdevname=0"
|
||
ks_rhel="http://$ipaddr/linux/ks/rhel.ks"
|
||
ks_dfmt="http://$ipaddr/linux/ks/dfmt.sh"
|
||
ks_ipxe="http://$ipaddr/linux/ks/ipxe.sh"
|
||
ks_init="http://$ipaddr/linux/ks/init.sh"
|
||
ks_repo="http://$ipaddr/linux/$url_os_path"
|
||
vmlinuz="http://$ipaddr/linux/$url_os_path/$kernel_path"
|
||
initrds="http://$ipaddr/linux/$url_os_path/$initrd_path"
|
||
|
||
{
|
||
echo "# ===================================================="
|
||
echo "# Generated by htool commands"
|
||
echo "# ===================================================="
|
||
echo ""
|
||
echo "interface $iface"
|
||
echo "server $ipaddr"
|
||
echo "pkgsroot $ks_dst"
|
||
echo "wwwroot $wwwroot"
|
||
echo "tftpboot $tftpboot"
|
||
echo "startip $start_ip"
|
||
echo "endip $ender_ip"
|
||
echo "dns 223.5.5.5"
|
||
echo "osname $sys_os_rels"
|
||
echo "osvers $sys_os_vers"
|
||
echo "ospath $url_os_path"
|
||
echo "lrepo $lrepo_path"
|
||
echo "bios $tftpboot/boot/undionly.kpxe"
|
||
echo "uefi $tftpboot/boot/bootx64.efi"
|
||
echo "dfmt $ks_dfmt"
|
||
echo "ksaddr $ks_rhel"
|
||
echo "ipxeboot $ks_ipxe"
|
||
echo "repos $ks_repo"
|
||
echo "vmlinuz $vmlinuz"
|
||
echo "initrd $initrds"
|
||
} > $config_file
|
||
|
||
pkgs_ipxe="$srv_root/htool/pkgs/ks/ipxe.sh"
|
||
sed -i "s|@osname@|$sys_os_rels|" $pkgs_ipxe
|
||
sed -i "s|@osrels@|"${sys_os_rels^}"|" $pkgs_ipxe
|
||
sed -i "s|@osvers@|$sys_os_vers|" $pkgs_ipxe
|
||
sed -i "s|@vmlinuz@|$vmlinuz|" $pkgs_ipxe
|
||
sed -i "s|@initrds@|$initrds|" $pkgs_ipxe
|
||
sed -i "s|@kargs@|$ks_args|" $pkgs_ipxe
|
||
sed -i "s|@repos@|$ks_repo|" $pkgs_ipxe
|
||
sed -i "s|@ksarg@|$ks_rhel|" $pkgs_ipxe
|
||
|
||
pkgs_rhel="$srv_root/htool/pkgs/ks/rhel.ks"
|
||
sed -i "s|@url@|$ks_repo|" $pkgs_rhel
|
||
sed -i "s|@dfmt@|$ks_dfmt|" $pkgs_rhel
|
||
sed -i "s|@init@|$ks_init|" $pkgs_rhel
|
||
|
||
|
||
pkgs_init="${ks_dst}ks/init.sh"
|
||
sed -i "s|@osipaddr@|$ipaddr|" $pkgs_init
|
||
sed -i "s|@ospath@|$url_os_path|" $pkgs_init
|
||
}
|
||
|
||
exec_run() {
|
||
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"
|
||
}
|
||
|
||
sync_run() {
|
||
log_debug "当前节点:${GLOBAL_NODES[*]}"
|
||
log_debug "公共参数:${PUBLIC_ARGS[*]}"
|
||
log_debug "私有参数:${PRIVATE_ARGS[*]}"
|
||
|
||
#case "${PRIVATE_ARGS[@]}" in
|
||
case "${GLOBAL_OPTS[@]}" in
|
||
all) echo 'all----->' ;;#sync_file_all ;;
|
||
file) echo 'file---->' ;;#sync_file_single ;;
|
||
*) show_help_sync ;;
|
||
esac
|
||
|
||
}
|
||
|
||
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
|
||
[[ $helper -ne 1 ]] && show_help
|
||
exit 0
|
||
fi
|
||
|
||
log_debug "$DSEP"
|
||
log_debug "解析子命令:${CMD_STACK[*]}"
|
||
log_debug "专属参数 :${PRIVATE_ARGS[*]}"
|
||
|
||
case "${CMD_STACK[0]}" in
|
||
exec)
|
||
exec_run "${CMD_STACK[1]}"
|
||
;;
|
||
init)
|
||
init_run "${CMD_STACK[1]}"
|
||
;;
|
||
sync)
|
||
case "${CMD_STACK[1]}" in
|
||
file) sync_run ;;
|
||
*) show_help ;;
|
||
esac
|
||
;;
|
||
show)
|
||
case "${CMD_STACK[1]}" in
|
||
node) show_nodes ;;
|
||
vimrc) show_vimrc ;;
|
||
system) show_system_summary ;;
|
||
*) show_help;;
|
||
esac
|
||
;;
|
||
build)
|
||
case "${CMD_STACK[1]}" in
|
||
httpd) build_httpd_srv ;;
|
||
slurm) build_slurm_srv ;;
|
||
dnsmasq) build_dnsmasq_srv ;;
|
||
*) show_help;;
|
||
esac
|
||
;;
|
||
create)
|
||
case "${CMD_STACK[1]}" in
|
||
syncfile) create_sync_file ;;
|
||
*) show_help ;;
|
||
esac
|
||
;;
|
||
*) [[ $helper -ne 1 ]] && 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 ;;
|
||
-m) GLOBAL_OPTS[mntroot]=$2; PUBLIC_ARGS+=("$arg" "$2"); shift 2 ;;
|
||
-w) GLOBAL_OPTS[wwwroot]=$2; PUBLIC_ARGS+=("$arg" "$2"); shift 2 ;;
|
||
-f) GLOBAL_OPTS[file]=$2; PUBLIC_ARGS+=("$arg" "$2"); shift 2 ;;
|
||
-a) GLOBAL_OPTS[all]=$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 ;;
|
||
--mntroot) GLOBAL_OPTS[mntroot]=$2; PUBLIC_ARGS+=("$arg" "$2"); shift 2 ;;
|
||
--wwwroot) GLOBAL_OPTS[wwwroot]=$2; PUBLIC_ARGS+=("$arg" "$2"); shift 2 ;;
|
||
--file) GLOBAL_OPTS[file]=$2; PUBLIC_ARGS+=("$arg" "$2"); shift 2 ;;
|
||
--all) GLOBAL_OPTS[all]=$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[@]}")
|
||
|
||
# 获取基础信息存储变量
|
||
if [[ ! -e $config_file ]]; then
|
||
info "$DSEP"
|
||
warn "配置文件不存在: $config_file "
|
||
warn "首次使用此工具需要使用下面命令进行初始化操作:"
|
||
warn " sunhpc init -i eth1 -m /mnt/cdrom"
|
||
info "$DSEP"
|
||
echo ""
|
||
helper=1
|
||
fi
|
||
|
||
[[ -e $config_file ]] && load_conf_envs
|
||
|
||
parse_args "$@"
|
||
dispatch
|
||
}
|
||
|
||
main "$@"
|