Files

353 lines
13 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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