From f97ea36872a7bba774eefdcfa219f6d622b7f026 Mon Sep 17 00:00:00 2001 From: seaHi Date: Wed, 30 Sep 2026 13:40:41 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20OpenStack=20?= =?UTF-8?q?=E8=AE=A1=E7=AE=97=E8=8A=82=E7=82=B9=E5=9F=BA=E7=A1=80=E7=8E=AF?= =?UTF-8?q?=E5=A2=83=E9=85=8D=E7=BD=AE=E6=A3=80=E6=B5=8B=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 3.5.sh | 369 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 369 insertions(+) create mode 100755 3.5.sh diff --git a/3.5.sh b/3.5.sh new file mode 100755 index 0000000..0c2fe34 --- /dev/null +++ b/3.5.sh @@ -0,0 +1,369 @@ +#!/bin/bash +# OpenStack 计算节点(CentOS 7.9 / Rocky)基础环境配置检测 +# 参考 3.4.sh:按手册章节汇总,异常提示对应步骤。 +# 用法:在 compute 上以 root 执行 bash 3.5.sh。 +# 可用 CHECK_CONTROLLER_IP 覆盖默认控制节点 IP(192.168.30.129)。 +# 仅检查;不修改配置、启停服务、执行重置脚本或清理/重建 YUM 缓存。 +# 退出码:0=全部通过,1=有失败,2=无失败但存在待人工核查项。 +# VMware 设置、远端状态及命令执行历史不能由本机完整验证,正常时仍可能返回 2。 + +export LANG=en_US.UTF-8 +export LC_ALL=en_US.UTF-8 +set -o pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +CYAN='\033[0;36m' +NC='\033[0m' +PASS_CNT=0 +FAIL_CNT=0 +SKIP_CNT=0 +SECTION_TITLE='' +SECTION_PASS=0 +SECTION_FAIL=0 +SECTION_SKIP=0 +SECTION_DETAILS=() + +# 正常检查仅计数;每章输出一行汇总,异常才展开具体步骤。 +pass() { PASS_CNT=$((PASS_CNT+1)); } +detail() { + if [ -n "$SECTION_TITLE" ]; then + SECTION_DETAILS+=("$1") + else + printf '%s\n' "$1" + fi +} +fail() { FAIL_CNT=$((FAIL_CNT+1)); detail " [✘ 失败] $1"; } +skip() { SKIP_CNT=$((SKIP_CNT+1)); detail " [− 跳过] $1"; } +section_summary() { + [ -n "$SECTION_TITLE" ] || return 0 + local passed=$((PASS_CNT-SECTION_PASS)) failed=$((FAIL_CNT-SECTION_FAIL)) skipped=$((SKIP_CNT-SECTION_SKIP)) + if [ "$failed" -gt 0 ]; then + printf '%b[✘] %s:通过 %s,失败 %s,跳过 %s%b\n' "$RED" "$SECTION_TITLE" "$passed" "$failed" "$skipped" "$NC" + elif [ "$skipped" -gt 0 ]; then + printf '%b[−] %s:通过 %s,跳过 %s%b\n' "$YELLOW" "$SECTION_TITLE" "$passed" "$skipped" "$NC" + else + printf '%b[✔] %s:%s 项通过%b\n' "$GREEN" "$SECTION_TITLE" "$passed" "$NC" + fi + if [ "${#SECTION_DETAILS[@]}" -gt 0 ]; then + printf '%s\n' "${SECTION_DETAILS[@]}" + fi +} +info() { + section_summary + SECTION_TITLE=$1 + SECTION_PASS=$PASS_CNT + SECTION_FAIL=$FAIL_CNT + SECTION_SKIP=$SKIP_CNT + SECTION_DETAILS=() +} + +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},请先补齐 CentOS 7 实验环境及 chrony 等检查工具。" + fi +done +if [ "$FAIL_CNT" -gt 0 ]; then + section_summary + printf '%b检查工具不齐,暂不能可靠检查;请补齐后重新运行。%b\n' "$RED" "$NC" + exit 1 +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 地址。' + section_summary + exit 1 +fi + +if grep -Eq '(^|[[:space:]])(vmx|svm)([[:space:]]|$)' /proc/cpuinfo 2>/dev/null; then + pass 'compute 可见 CPU 硬件虚拟化扩展' +else + fail '[准备工作第 2、3 步] compute 未检测到 vmx/svm,请检查克隆后的虚拟机是否启用嵌套虚拟化。' +fi +skip '[准备工作第 1~4 步] 请在 VMware 中核对 VMnet1/VMnet8、controller 虚拟化开关、克隆来源和两台虚拟机的电源状态;本机无法完整确认。' + +info '一、重置控制节点环境' +skip '[第 1 步] 无法从 compute 证明 controller 已执行 env3-4-local.sh;请在 controller 核查重置结果,并运行 3.4.sh 验证服务。' + +info '二、基础安全设置' +state=$(systemctl is-active firewalld 2>/dev/null) +case "$state" in + inactive) pass 'firewalld 已停止';; + *) fail "[第 1 步] firewalld 状态为 ${state:-查询失败},应为 inactive;请检查 systemctl status firewalld。";; +esac +state=$(systemctl is-enabled firewalld 2>/dev/null) +case "$state" in + disabled|masked) pass 'firewalld 已禁止开机启动';; + *) fail "[第 1 步] firewalld 自启状态为 ${state:-查询失败},应为 disabled 或 masked。";; +esac +case "$(getenforce 2>/dev/null)" in + Disabled|Permissive) pass 'SELinux 当前未强制执行';; + *) fail '[第 2 步] SELinux 仍为 Enforcing 或状态查询失败,请检查 setenforce 0 的执行结果。';; +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 '[第 3 步] /etc/selinux/config 的有效 SELINUX 值应为 disabled。' +fi + +info '三、配置主机名解析' +if [ "$(hostname 2>/dev/null)" = compute ]; then + pass '当前主机名为 compute' +else + fail '[计算节点第 1 步] 当前主机名应为 compute,请检查 hostnamectl set-hostname compute。' +fi +if [ "$(tr -d '[:space:]' 2>/dev/null < /etc/hostname)" = compute ]; then + pass '持久主机名为 compute' +else + fail '[计算节点第 1 步] /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 "[两节点 hosts 配置] 本机 /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 '[计算节点 hosts 配置] compute 应唯一映射到本机实际管理 IPv4 地址,不能使用 XXX、controller 地址或回环地址。' +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 的实际系统解析与 hosts 不一致或解析失败,请检查 /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},请检查地址、网卡、路由及 ICMP 是否被过滤。" + fi +done +skip '[控制节点 hosts 配置及反向连通性] 请在 controller 核对 compute 的实际 IP,并执行 ping compute;本机 ping 成功不能证明反向解析正确。' + +info '四、配置 NTP 时间同步' +if rpm -q chrony >/dev/null 2>&1; then + pass 'chrony 已安装' +else + fail '[第 1 步] 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 '[第 2 步] /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 '[第 2 步] 请注释默认外网 server/pool 及其他时间源,仅保留 controller。' +fi +if systemctl is-active --quiet chronyd; then + pass 'chronyd 正在运行' +else + fail '[第 3 步] chronyd 未运行。' +fi +if [ "$(systemctl is-enabled chronyd 2>/dev/null)" = enabled ]; then + pass 'chronyd 已持久设置开机自启' +else + fail '[第 3 步] 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 "[第 4 步] 未看到 ^* ${CONTROLLER_IP};请运行 chronyc -n sources,检查 controller 的 NTP 服务、UDP 123 及网络;刚启动可稍后重试。" + fi + if printf '%s\n' "$sources" | awk -v ip="$CONTROLLER_IP" '$1 ~ /^[\^=]/ && $2!=ip { bad=1 } END { exit bad }'; then + pass '运行中的 chronyd 未加载其他服务器或对等时间源' + else + fail '[第 2、3 步] chronyd 仍加载其他时间源,请检查 include/sourcedir 配置及修改后是否重启服务。' + fi +else + fail '[第 4 步] 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 '])' && \ + printf '%s\n' "$metadata" | grep -Fq ''; then + pass "$id 的仓库索引可访问" + else + fail "[镜像连通性] 无法获取 $id 的有效 repomd.xml,请检查 VMnet8、镜像服务器和路径 ${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 "[清理并重建 YUM 缓存] $id 的缓存查询失败或超时;配置正确且网络连通后,请执行 yum clean all 和 yum makecache。" + fi + else + skip "[YUM 缓存] $id 配置未通过,修复后再验证缓存。" + 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 + +section_summary +printf '\n合计:通过 %b%s%b,失败 %b%s%b,跳过 %b%s%b\n' \ + "$GREEN" "$PASS_CNT" "$NC" "$RED" "$FAIL_CNT" "$NC" "$YELLOW" "$SKIP_CNT" "$NC" +printf '说明:检查当前配置及缓存可用性,不能证明曾执行过重置、克隆或 yum clean all/makecache 命令。\n' +if [ "$FAIL_CNT" -gt 0 ]; then + printf '%b请按异常项的手册步骤修正后重试。%b\n' "$RED" "$NC" + exit 1 +elif [ "$SKIP_CNT" -gt 0 ]; then + printf '%b本机已检查项通过;仍有待人工核查内容,请按跳过提示确认。%b\n' "$YELLOW" "$NC" + exit 2 +else + exit 0 +fi