353 lines
13 KiB
Bash
Executable File
353 lines
13 KiB
Bash
Executable File
#!/bin/bash
|
||
# OpenStack 计算节点(CentOS 7.9 / Rocky)基础环境配置检测
|
||
# 用法:在 compute 上以 root 执行 bash 3.5.sh。
|
||
# 可用 CHECK_CONTROLLER_IP 覆盖默认控制节点 IP(192.168.30.129)。
|
||
# 仅检查;不修改配置、启停服务、执行重置脚本或清理/重建 YUM 缓存。
|
||
# 退出码:0=本次检查项全部通过,1=有失败。
|
||
# 仅检查本机可验证的当前状态,不评价 VMware 设置、远端操作或命令历史。
|
||
|
||
export LANG=en_US.UTF-8
|
||
export LC_ALL=en_US.UTF-8
|
||
set -o pipefail
|
||
|
||
# 学生可见输出:每组一行,失败时列出原因;仅统计实际检查结果。
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
CYAN='\033[0;36m'
|
||
NC='\033[0m'
|
||
PASS_CNT=0
|
||
FAIL_CNT=0
|
||
SECTION_TITLE=''
|
||
SECTION_PASS=0
|
||
SECTION_FAIL=0
|
||
SECTION_DETAILS=()
|
||
|
||
pass() { PASS_CNT=$((PASS_CNT+1)); }
|
||
fail() {
|
||
FAIL_CNT=$((FAIL_CNT+1))
|
||
if [ -n "$SECTION_TITLE" ]; then
|
||
SECTION_DETAILS+=("$1")
|
||
else
|
||
printf '%b[✘ 失败] %s%b\n' "$RED" "$1" "$NC"
|
||
fi
|
||
}
|
||
section_summary() {
|
||
[ -n "$SECTION_TITLE" ] || return 0
|
||
local passed=$((PASS_CNT-SECTION_PASS)) failed=$((FAIL_CNT-SECTION_FAIL)) message
|
||
# 没有实际检查结果的分组不显示,也不记为通过。
|
||
if [ "$failed" -gt 0 ]; then
|
||
printf '%b[✘ 失败] %s:通过 %s,失败 %s%b\n' "$RED" "$SECTION_TITLE" "$passed" "$failed" "$NC"
|
||
for message in "${SECTION_DETAILS[@]}"; do
|
||
printf '%b · %s%b\n' "$RED" "$message" "$NC"
|
||
done
|
||
elif [ "$passed" -gt 0 ]; then
|
||
printf '%b[✔ 通过] %s(%s 项)%b\n' "$GREEN" "$SECTION_TITLE" "$passed" "$NC"
|
||
fi
|
||
}
|
||
info() {
|
||
section_summary
|
||
SECTION_TITLE=$1
|
||
SECTION_PASS=$PASS_CNT
|
||
SECTION_FAIL=$FAIL_CNT
|
||
SECTION_DETAILS=()
|
||
}
|
||
finish() {
|
||
section_summary
|
||
printf '\n检查完毕:通过 %b%s%b 项,失败 %b%s%b 项。\n' \
|
||
"$GREEN" "$PASS_CNT" "$NC" "$RED" "$FAIL_CNT" "$NC"
|
||
if [ "$FAIL_CNT" -gt 0 ]; then
|
||
printf '%b请按失败提示修正后重试。%b\n' "$RED" "$NC"
|
||
exit 1
|
||
fi
|
||
printf '%b本次检查项全部通过。%b\n' "$GREEN" "$NC"
|
||
exit 0
|
||
}
|
||
|
||
printf '%bOpenStack 计算节点基础环境自测%b\n' "$CYAN" "$NC"
|
||
|
||
if [ "$EUID" -ne 0 ]; then
|
||
fail '请在计算节点 compute 上使用 root 权限执行此脚本。'
|
||
exit 1
|
||
fi
|
||
|
||
CONTROLLER_IP=${CHECK_CONTROLLER_IP:-192.168.30.129}
|
||
REPO_DIR=/etc/yum.repos.d
|
||
|
||
# 不能把命令不存在、查询报错当成“服务已关闭”。
|
||
info '准备工作:检查工具与虚拟化'
|
||
for cmd in awk sort grep tr hostname ip getent ping rpm systemctl getenforce chronyc timeout curl yum; do
|
||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||
fail "缺少命令 ${cmd},请安装对应软件包"
|
||
fi
|
||
done
|
||
if [ "$FAIL_CNT" -gt 0 ]; then
|
||
finish
|
||
fi
|
||
|
||
valid_ipv4() {
|
||
printf '%s\n' "$1" | awk -F. '
|
||
NF != 4 { bad=1 }
|
||
{ for (i=1; i<=NF; i++) if ($i !~ /^[0-9]+$/ || length($i)>3 || $i+0>255) bad=1 }
|
||
END { exit (NR!=1 || bad) }
|
||
'
|
||
}
|
||
if ! valid_ipv4 "$CONTROLLER_IP"; then
|
||
fail 'CHECK_CONTROLLER_IP 必须为有效的 IPv4 地址。'
|
||
finish
|
||
fi
|
||
|
||
if grep -Eq '(^|[[:space:]])(vmx|svm)([[:space:]]|$)' /proc/cpuinfo 2>/dev/null; then
|
||
pass 'compute 可见 CPU 硬件虚拟化扩展'
|
||
else
|
||
fail '未检测到 vmx/svm,请启用嵌套虚拟化'
|
||
fi
|
||
|
||
info '基础安全设置'
|
||
state=$(systemctl is-active firewalld 2>/dev/null)
|
||
case "$state" in
|
||
inactive) pass 'firewalld 已停止';;
|
||
*) fail "firewalld 状态为 ${state:-查询失败},应为 inactive";;
|
||
esac
|
||
state=$(systemctl is-enabled firewalld 2>/dev/null)
|
||
case "$state" in
|
||
disabled|masked) pass 'firewalld 已禁止开机启动';;
|
||
*) fail "firewalld 自启状态为 ${state:-查询失败},应为 disabled 或 masked。";;
|
||
esac
|
||
case "$(getenforce 2>/dev/null)" in
|
||
Disabled|Permissive) pass 'SELinux 当前未强制执行';;
|
||
*) fail 'SELinux 仍为 Enforcing 或状态查询失败';;
|
||
esac
|
||
# 读取最后一个有效赋值,忽略注释;不 source 系统配置。
|
||
selinux=$(awk '
|
||
/^[[:space:]]*#/ { next }
|
||
/^[[:space:]]*SELINUX[[:space:]]*=/ {
|
||
v=$0; sub(/^[^=]*=/,"",v); sub(/[[:space:]]*#.*/,"",v)
|
||
gsub(/[[:space:]"\047]/,"",v)
|
||
}
|
||
END { print v }
|
||
' /etc/selinux/config 2>/dev/null)
|
||
if [ "$selinux" = disabled ]; then
|
||
pass 'SELinux 永久配置为 disabled'
|
||
else
|
||
fail '/etc/selinux/config 的有效 SELINUX 值应为 disabled。'
|
||
fi
|
||
|
||
info '配置主机名解析'
|
||
if [ "$(hostname 2>/dev/null)" = compute ]; then
|
||
pass '当前主机名为 compute'
|
||
else
|
||
fail '当前主机名应为 compute,请检查 hostnamectl set-hostname compute。'
|
||
fi
|
||
if [ "$(tr -d '[:space:]' 2>/dev/null < /etc/hostname)" = compute ]; then
|
||
pass '持久主机名为 compute'
|
||
else
|
||
fail '/etc/hostname 应保存 compute,避免重启后主机名恢复。'
|
||
fi
|
||
LOCAL_IPS=$(ip -o -4 addr show scope global 2>/dev/null | awk '{split($4,a,"/"); print a[1]}')
|
||
hosts_addresses() {
|
||
awk -v name="$1" '{ sub(/#.*/, ""); for(i=2;i<=NF;i++) if($i==name) print $1 }' /etc/hosts 2>/dev/null | sort -u
|
||
}
|
||
COMPUTE_IP=$(hosts_addresses compute)
|
||
if [ "$(hosts_addresses controller)" = "$CONTROLLER_IP" ]; then
|
||
pass '/etc/hosts 中 controller 映射正确且无冲突'
|
||
else
|
||
fail "/etc/hosts 中 controller 应唯一映射到 ${CONTROLLER_IP}"
|
||
fi
|
||
if valid_ipv4 "$COMPUTE_IP" && [ "$COMPUTE_IP" != "$CONTROLLER_IP" ] && \
|
||
printf '%s\n' "$LOCAL_IPS" | grep -Fxq "$COMPUTE_IP"; then
|
||
pass '/etc/hosts 中 compute 映射到本机 IPv4 地址'
|
||
else
|
||
fail '/etc/hosts 中 compute 应唯一映射到本机管理 IPv4 地址'
|
||
fi
|
||
for name in controller compute; do
|
||
expected=$CONTROLLER_IP
|
||
[ "$name" != compute ] || expected=$COMPUTE_IP
|
||
resolved=$(timeout 5 getent ahostsv4 "$name" 2>/dev/null | awk '{print $1}' | sort -u)
|
||
if [ -n "$expected" ] && [ "$resolved" = "$expected" ]; then
|
||
pass "$name 的系统 IPv4 解析与 hosts 一致"
|
||
else
|
||
fail "$name 解析异常,请检查 /etc/hosts 和 /etc/nsswitch.conf"
|
||
fi
|
||
if timeout 8 ping -4 -c 2 -W 2 "$name" >/dev/null 2>&1; then
|
||
pass "可通过主机名 ping 通 $name"
|
||
else
|
||
fail "ping ${name} 失败,请检查地址和网络"
|
||
fi
|
||
done
|
||
|
||
info '配置 NTP 时间同步'
|
||
if rpm -q chrony >/dev/null 2>&1; then
|
||
pass 'chrony 已安装'
|
||
else
|
||
fail 'chrony 软件包未安装。'
|
||
fi
|
||
if awk '
|
||
{ sub(/[#!;].*/, "") }
|
||
$1 == "server" && $2 == "controller" {
|
||
count++; for(i=3;i<=NF;i++) if($i=="iburst") valid++
|
||
}
|
||
END { exit !(count==1 && valid==1) }
|
||
' /etc/chrony.conf 2>/dev/null; then
|
||
pass 'chrony 配置包含唯一的 server controller iburst'
|
||
else
|
||
fail '/etc/chrony.conf 应有唯一的有效 server controller iburst 配置。'
|
||
fi
|
||
if awk '
|
||
{ sub(/[#!;].*/, "") }
|
||
($1=="server" || $1=="pool" || $1=="peer") && !($1=="server" && $2=="controller") { bad=1 }
|
||
END { exit bad }
|
||
' /etc/chrony.conf 2>/dev/null; then
|
||
pass 'chrony 主配置中没有其他启用的时间源'
|
||
else
|
||
fail '请注释默认外网 server/pool 及其他时间源,仅保留 controller。'
|
||
fi
|
||
if systemctl is-active --quiet chronyd 2>/dev/null; then
|
||
pass 'chronyd 正在运行'
|
||
else
|
||
fail 'chronyd 未运行。'
|
||
fi
|
||
if [ "$(systemctl is-enabled chronyd 2>/dev/null)" = enabled ]; then
|
||
pass 'chronyd 已持久设置开机自启'
|
||
else
|
||
fail 'chronyd 未设置持久的开机自启。'
|
||
fi
|
||
# 使用 -n 比较实际 IP,兼容显示为 controller、FQDN 或 IP 的情况。
|
||
if sources=$(timeout 10 chronyc -n sources 2>/dev/null); then
|
||
if printf '%s\n' "$sources" | awk -v ip="$CONTROLLER_IP" '$1=="^*" && $2==ip { found=1 } END { exit !found }'; then
|
||
pass 'chronyd 当前已选中 controller 为同步源'
|
||
else
|
||
fail "尚未同步到 ${CONTROLLER_IP},请检查 chronyc -n sources"
|
||
fi
|
||
if printf '%s\n' "$sources" | awk -v ip="$CONTROLLER_IP" '$1 ~ /^[\^=]/ && $2!=ip { bad=1 } END { exit bad }'; then
|
||
pass '运行中的 chronyd 未加载其他服务器或对等时间源'
|
||
else
|
||
fail 'chronyd 仍加载其他时间源,请检查配置并重启服务'
|
||
fi
|
||
else
|
||
fail 'chronyc 查询失败或超时,请检查 chronyd。'
|
||
fi
|
||
|
||
info '配置软件源与 YUM 缓存'
|
||
# 按指定文件、节读取键值,避免注释及其他节同名键误判。
|
||
repo_value() {
|
||
awk -v section="$2" -v key="$3" '
|
||
/^[[:space:]]*[#;]/ { next }
|
||
{ sub(/\r$/, "") }
|
||
/^[[:space:]]*\[/ {
|
||
s=$0; sub(/^[[:space:]]*\[/,"",s); sub(/\].*$/,"",s)
|
||
gsub(/^[[:space:]]+|[[:space:]]+$/,"",s); next
|
||
}
|
||
s==section && index($0,"=") {
|
||
k=substr($0,1,index($0,"=")-1); gsub(/^[[:space:]]+|[[:space:]]+$/,"",k)
|
||
if(k==key) {
|
||
v=substr($0,index($0,"=")+1)
|
||
sub(/[[:space:]]+[#;].*$/,"",v)
|
||
gsub(/^[[:space:]]+|[[:space:]]+$/,"",v)
|
||
}
|
||
}
|
||
END { print v }
|
||
' "$1" 2>/dev/null
|
||
}
|
||
shopt -s nullglob
|
||
REPO_FILES=("$REPO_DIR"/*.repo)
|
||
if [ "${#REPO_FILES[@]}" -gt 0 ]; then
|
||
extra_repos=$(awk '
|
||
function flush() {
|
||
if (s!="" && enabled!="0" && s!="local-base" && s!="local-updates" &&
|
||
s!="local-extras" && s!="local-qemu-ev" && s!="local-openstack-rocky") print s
|
||
}
|
||
FNR==1 { flush(); s=""; enabled="1" }
|
||
/^[[:space:]]*[#;]/ { next }
|
||
{ sub(/\r$/, "") }
|
||
/^[[:space:]]*\[/ {
|
||
flush(); s=$0; sub(/^[[:space:]]*\[/,"",s); sub(/\].*$/,"",s)
|
||
gsub(/^[[:space:]]+|[[:space:]]+$/,"",s); enabled="1"; next
|
||
}
|
||
/^[[:space:]]*enabled[[:space:]]*=/ {
|
||
enabled=$0; sub(/^[^=]*=/,"",enabled); sub(/[[:space:]]+[#;].*$/,"",enabled)
|
||
gsub(/^[[:space:]]+|[[:space:]]+$/,"",enabled)
|
||
}
|
||
END { flush() }
|
||
' "${REPO_FILES[@]}" | sort -u)
|
||
if [ -z "$extra_repos" ]; then
|
||
pass '未遗留其他启用的软件源'
|
||
else
|
||
fail "仍有额外启用的仓库:$(printf '%s' "$extra_repos" | tr '\n' ' ')"
|
||
fi
|
||
fi
|
||
while IFS='|' read -r file id path; do
|
||
url="http://192.168.192.205:3080/$path/"
|
||
config_ok=1
|
||
if [ ! -r "$REPO_DIR/$file" ]; then
|
||
fail "缺少或无法读取 $REPO_DIR/${file}。"
|
||
config_ok=0
|
||
else
|
||
for key in baseurl enabled gpgcheck; do
|
||
case "$key" in baseurl) expected=$url;; enabled) expected=1;; gpgcheck) expected=0;; esac
|
||
value=$(repo_value "$REPO_DIR/$file" "$id" "$key")
|
||
[ "$key" != baseurl ] || value="${value%/}/"
|
||
if [ "$value" = "$expected" ]; then
|
||
pass "$id 的 $key 正确"
|
||
else
|
||
fail "$file 的 [$id] $key 应为 ${expected}。"
|
||
config_ok=0
|
||
fi
|
||
done
|
||
for key in mirrorlist metalink; do
|
||
if [ -n "$(repo_value "$REPO_DIR/$file" "$id" "$key")" ]; then
|
||
fail "[$id] 存在额外的 ${key},请按手册仅使用本地 baseurl。"
|
||
config_ok=0
|
||
fi
|
||
done
|
||
if [ "$id" = local-openstack-rocky ]; then
|
||
excludes=$(repo_value "$REPO_DIR/$file" "$id" exclude | tr ',' ' ')
|
||
if printf '%s\n' "$excludes" | awk '{for(i=1;i<=NF;i++){if($i=="sip")s=1;if($i=="PyQt4")p=1}} END{exit !(s&&p)}'; then
|
||
pass 'Rocky 源已排除 sip 和 PyQt4'
|
||
else
|
||
fail '[local-openstack-rocky] 应设置 exclude=sip,PyQt4。'
|
||
config_ok=0
|
||
fi
|
||
fi
|
||
fi
|
||
count=0
|
||
if [ "${#REPO_FILES[@]}" -gt 0 ]; then
|
||
count=$(awk -v id="$id" '
|
||
/^[[:space:]]*\[/ {
|
||
s=$0; sub(/^[[:space:]]*\[/,"",s); sub(/\].*$/,"",s)
|
||
gsub(/^[[:space:]]+|[[:space:]]+$/,"",s); if(s==id)n++
|
||
} END { print n+0 }
|
||
' "${REPO_FILES[@]}")
|
||
fi
|
||
if [ "$count" = 1 ]; then
|
||
pass "$id 定义唯一"
|
||
else
|
||
fail "$id 在 .repo 文件中出现 $count 次,应只定义一次。"
|
||
config_ok=0
|
||
fi
|
||
# GET 元数据而非仅访问目录,避免把空目录/404 当成仓库可用。
|
||
if metadata=$(curl --noproxy '*' -fsSL --connect-timeout 3 --max-time 10 --max-filesize 1048576 "${url}repodata/repomd.xml" 2>/dev/null) && \
|
||
printf '%s\n' "$metadata" | grep -Eq '<repomd([[:space:]>])' && \
|
||
printf '%s\n' "$metadata" | grep -Fq '</repomd>'; then
|
||
pass "$id 的仓库索引可访问"
|
||
else
|
||
fail "$id 仓库索引访问失败,请检查 ${url}"
|
||
fi
|
||
if [ "$config_ok" -eq 1 ]; then
|
||
# -C 使用已有缓存;禁用插件,不执行 clean/makecache,也不下载软件包。
|
||
if timeout 20 yum -C --noplugins --disablerepo='*' --enablerepo="$id" \
|
||
--setopt="$id.skip_if_unavailable=0" list available >/dev/null 2>&1; then
|
||
pass "$id 的现有 YUM 缓存可读取"
|
||
else
|
||
fail "$id 缓存查询失败,请检查网络并重建 YUM 缓存"
|
||
fi
|
||
fi
|
||
done <<'REPOS'
|
||
CentOS-Base.repo|local-base|vault-base
|
||
CentOS-Base.repo|local-updates|vault-updates
|
||
CentOS-Base.repo|local-extras|vault-extras
|
||
CentOS-QEMU-EV.repo|local-qemu-ev|vault-centos-qemu-ev
|
||
CentOS-OpenStack-rocky.repo|local-openstack-rocky|vault-centos-openstack-rocky
|
||
REPOS
|
||
|
||
finish
|