feat: 添加 OpenStack 3-5 计算节点自动化评分与自测脚本

This commit is contained in:
seahi committed 2026-10-09 14:45:23 +08:00
1 parent dcbfac4bed
commit fc063cd56c
1 file changed
+306
Executable
+306
View File
@@ -0,0 +1,306 @@
#!/bin/bash
# 3-5 计算节点评分:CentOS 7.9 / OpenStack Rocky,Bash 4+,满分 100。
# 用法:在 compute 上执行 bash 3-5-compute.sh
# 可选:CHECK_CONTROLLER_IP=控制节点管理IP(默认从 hosts 读取,否则 192.168.30.129)
# CHECK_COMPUTE_IP=计算节点管理IP(默认从 hosts 读取)
# CHECK_REPO_BASE_URL=软件源根URL(默认 http://192.168.192.205:3080)
# 只查询当前状态;不安装软件、改配置、重启服务或重建缓存。
# 无法从节点内证明 VMware 操作、克隆或命令历史,不为这些过程评分。
# 退出码:0=100分,1=有扣分,2=权限/基础工具/参数错误,未完成评分。
# 分值:安全8、主机与网络12、软件源15、NTP15、Nova软件包5、
# Nova配置30、虚拟化5、服务10。每个配置小组内全部通过才获得该小组分值。
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
set -o pipefail
SCORE=0
TOTAL=0
PASS_CNT=0
FAIL_CNT=0
TASK_CNT=0
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[0;33m'
NC='\033[0m'
separator() {
printf '%b====================================================%b\n' "$YELLOW" "$NC"
}
banner() {
separator
printf '%b %s%b\n' "$YELLOW" "$1" "$NC"
separator
}
section() {
TASK_CNT=$((TASK_CNT+1))
printf '%b[➜] %d. 检查任务:%s%b\n' "$BLUE" "$TASK_CNT" "$1" "$NC"
}
error() {
printf '%b[✘ 失败] %s%b\n' "$RED" "$1" "$NC"
}
# 每个评分项分值固定;查询失败/依赖缺失也计入满分,不跳项缩小分母。
# 通过和失败各自使用明确的提示,逐项实时显示。
check() {
local points=$1 success_message=$2 failure_message=$3
shift 3
TOTAL=$((TOTAL+points))
if "$@" >/dev/null 2>&1; then
SCORE=$((SCORE+points)); PASS_CNT=$((PASS_CNT+1))
printf '%b[✔ 通过] %s(得 %d 分)%b\n' "$GREEN" "$success_message" "$points" "$NC"
else
FAIL_CNT=$((FAIL_CNT+1))
printf '%b[✘ 失败] %s(扣 %d 分)%b\n' "$RED" "$failure_message" "$points" "$NC"
fi
}
finish() {
printf '\n'
separator
printf '实验自测检查完毕!通过项: %b%d%b,失败项: %b%d%b\n' "$GREEN" "$PASS_CNT" "$NC" "$RED" "$FAIL_CNT" "$NC"
printf '%b总成绩:%d/100 分%b\n' "$YELLOW" "$SCORE" "$NC"
if [ "$TOTAL" -ne 100 ]; then
error "评分项总分异常:$TOTAL,请联系教师检查脚本。"
separator
exit 2
fi
if [ "$FAIL_CNT" -eq 0 ]; then
printf '%b所有检查项均已通过,实验自测完成!%b\n' "$GREEN" "$NC"
separator
exit 0
fi
printf '%b你有 %d 处检查未通过,请根据上方红字提示核对实验步骤并修正!%b\n' "$RED" "$FAIL_CNT" "$NC"
separator
exit 1
}
preflight() {
if [ "$EUID" -ne 0 ]; then error '请以 root 身份在对应节点执行。未评分。'; exit 2; fi
local cmd
for cmd in awk grep sort tr timeout; do
if ! command -v "$cmd" >/dev/null 2>&1; then error "缺少基础工具 $cmd,无法评分。"; exit 2; fi
done
}
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) }'
}
hosts_ip() {
awk -v name="$1" '{sub(/#.*/, ""); for(i=2;i<=NF;i++) if($i==name) print $1}' /etc/hosts 2>/dev/null | sort -u
}
resolved_as() {
local addresses
valid_ipv4 "$2" || return 1
addresses=$(timeout 5 getent ahostsv4 "$1" 2>/dev/null | awk '{print $1}' | sort -u) || return 1
[ "$addresses" = "$2" ]
}
# 按节读取最后一个有效赋值;忽略整行注释,不执行配置文件。
ini_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)
gsub(/^[[:space:]]+|[[:space:]]+$/,"",v)
}
}
END {print v}' "$1" 2>/dev/null
}
enabled_service() { [ "$(timeout 10 systemctl is-enabled "$1" 2>/dev/null)" = enabled ]; }
active_service() { timeout 10 systemctl is-active --quiet "$1"; }
banner 'OpenStack 计算节点实验自测与自动化评分脚本'
preflight
CONF=/etc/nova/nova.conf
# 自动检测失败(例如 hosts 冲突)仍继续评分,让解析项正常扣分。
CONTROLLER_IP=${CHECK_CONTROLLER_IP:-$(hosts_ip controller)}
if [ -z "${CHECK_CONTROLLER_IP:-}" ] && ! valid_ipv4 "$CONTROLLER_IP"; then
CONTROLLER_IP=192.168.30.129
fi
COMPUTE_IP=${CHECK_COMPUTE_IP:-$(hosts_ip compute)}
REPO_BASE=${CHECK_REPO_BASE_URL:-http://192.168.192.205:3080}
REPO_BASE=${REPO_BASE%/}
if ! valid_ipv4 "$CONTROLLER_IP"; then error '控制节点 IP 参数或 hosts 映射无效。未评分。'; exit 2; fi
if [ -n "${CHECK_COMPUTE_IP:-}" ] && ! valid_ipv4 "$CHECK_COMPUTE_IP"; then error 'CHECK_COMPUTE_IP 无效。未评分。'; exit 2; fi
case "$REPO_BASE" in http://*|https://*) ;; *) error '软件源根 URL 必须使用 http(s)。未评分。'; exit 2;; esac
section '基础安全设置(8分)'
firewall_stopped() { [ "$(timeout 10 systemctl is-active firewalld 2>/dev/null)" = inactive ]; }
firewall_disabled() {
local state
state=$(timeout 10 systemctl is-enabled firewalld 2>/dev/null)
case "$state" in disabled|masked) return 0;; *) return 1;; esac
}
selinux_runtime() { case "$(getenforce 2>/dev/null)" in Disabled|Permissive) return 0;; *) return 1;; esac; }
selinux_persistent() {
awk '/^[[:space:]]*#/ {next}
/^[[:space:]]*SELINUX[[:space:]]*=/ {
v=$0; sub(/^[^=]*=/,"",v); sub(/[[:space:]]*#.*/,"",v); gsub(/[[:space:]"\047]/,"",v)
} END {exit (v!="disabled")}' /etc/selinux/config
}
check 2 'firewalld 防火墙已停止' 'firewalld 应停止,查询失败也不得分' firewall_stopped
check 2 'firewalld 防火墙已禁止开机启动' 'firewalld 应禁止开机启动' firewall_disabled
check 2 'SELinux 当前为 Disabled 或 Permissive' 'SELinux 当前应为 Disabled 或 Permissive' selinux_runtime
check 2 'SELinux 持久配置为 disabled' '/etc/selinux/config 应设置 SELINUX=disabled' selinux_persistent
section '主机名、解析与网络(12分)'
hostname_ok() { [ "$(hostname)" = compute ] && [ "$(tr -d '[:space:]' < /etc/hostname)" = compute ]; }
controller_mapping() { [ "$(hosts_ip controller)" = "$CONTROLLER_IP" ]; }
compute_mapping() {
valid_ipv4 "$COMPUTE_IP" && [ "$COMPUTE_IP" != "$CONTROLLER_IP" ] &&
[ "$(hosts_ip compute)" = "$COMPUTE_IP" ] &&
ip -o -4 addr show scope global | awk -v ip="$COMPUTE_IP" '{split($4,a,"/"); if(a[1]==ip)found=1} END{exit !found}'
}
both_resolve() { resolved_as controller "$CONTROLLER_IP" && resolved_as compute "$COMPUTE_IP"; }
localhost_preserved() { awk '{sub(/#.*/,""); if($1=="127.0.0.1")for(i=2;i<=NF;i++)if($i=="localhost")found=1} END{exit !found}' /etc/hosts; }
check 2 '运行中及持久主机名均为 compute' '运行中及持久主机名都应为 compute' hostname_ok
check 2 'controller 的 hosts 映射唯一,控制节点 IP 配置正确' '/etc/hosts 中 controller 应唯一映射到控制节点 IP' controller_mapping
check 2 'compute 的 hosts 映射唯一,且指向本机管理 IPv4' '/etc/hosts 中 compute 应唯一映射到本机管理 IPv4' compute_mapping
check 2 'controller 和 compute 的系统解析与 hosts 配置一致' 'controller/compute 的系统解析应与 hosts 一致' both_resolve
check 2 'hosts 中保留了 localhost 的 IPv4 映射' '新增 hosts 条目时应保留 localhost 的 IPv4 映射' localhost_preserved
check 2 '可通过主机名 ping 通 controller' '应能通过主机名 ping 通 controller' timeout 8 ping -4 -c 2 -W 2 controller
section '软件源配置、连通性与缓存(15分)'
# 每个仓库分别评价配置、repomd 索引、已有缓存,各1分。
shopt -s nullglob
REPOS=(/etc/yum.repos.d/*.repo)
repo_layout_ok() {
[ "${#REPOS[@]}" -gt 0 ] || return 1
awk '
function flush() {
if(s!="" && enabled!="0" && s!="local-base" && s!="local-updates" &&
s!="local-extras" && s!="local-qemu-ev" && s!="local-openstack-rocky") bad=1
}
FNR==1 {flush(); s=""; enabled="1"}
/^[[:space:]]*[#;]/ {next}
/^[[: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(); exit bad}' "${REPOS[@]}"
}
repo_ok() {
local file=$1 id=$2 url=$3 key value count excludes
[ -r "$file" ] && repo_layout_ok || return 1
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}' "${REPOS[@]}") || return 1
[ "$count" = 1 ] || return 1
value=$(ini_value "$file" "$id" baseurl)
[ "${value%/}" = "${url%/}" ] && [ "$(ini_value "$file" "$id" enabled)" = 1 ] &&
[ "$(ini_value "$file" "$id" gpgcheck)" = 0 ] || return 1
for key in mirrorlist metalink; do [ -z "$(ini_value "$file" "$id" "$key")" ] || return 1; done
if [ "$id" = local-openstack-rocky ]; then
excludes=$(ini_value "$file" "$id" exclude | tr ',' ' ')
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)}' || return 1
fi
}
repo_online() {
local data
data=$(curl --noproxy '*' -fsSL --connect-timeout 3 --max-time 10 --max-filesize 1048576 "$1/repodata/repomd.xml") || return 1
printf '%s\n' "$data" | grep -Eq '<repomd([[:space:]>])' && printf '%s\n' "$data" | grep -Fq '</repomd>'
}
repo_cache() {
timeout 20 yum -C --noplugins --disablerepo='*' --enablerepo="$1" --setopt="$1.skip_if_unavailable=0" list available
}
while IFS='|' read -r file id path; do
url="$REPO_BASE/$path"
check 1 "[$id] 软件源配置正确,定义唯一,且未遗留其他启用源" "[$id] 配置缺失、重复或遗留其他启用源(Rocky 还需 exclude=sip,PyQt4)" repo_ok "/etc/yum.repos.d/$file" "$id" "$url"
check 1 "[$id] 仓库索引可正常访问:$url" "[$id] 仓库索引不可访问:$url" repo_online "$url"
check 1 "[$id] 已有 YUM 缓存可正常读取" "[$id] 已有 YUM 缓存不可读取,请检查 yum makecache 结果" repo_cache "$id"
done <<'REPOLIST'
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
REPOLIST
section 'NTP 时间同步(15分)'
chrony_config() {
awk '{sub(/[#!;].*/,"")}
$1=="server" && $2=="controller" {n++; for(i=3;i<=NF;i++)if($i=="iburst")good++}
($1=="server" || $1=="pool" || $1=="peer") && !($1=="server" && $2=="controller") {bad=1}
END{exit !(n==1 && good==1 && !bad)}' /etc/chrony.conf
}
chrony_synced() {
local sources
sources=$(timeout 10 chronyc -n sources) || return 1
printf '%s\n' "$sources" | awk -v ip="$CONTROLLER_IP" '
$1=="^*" && $2==ip {found=1}
$1 ~ /^[\^=]/ && $2!=ip {bad=1}
END{exit !(found && !bad)}'
}
check 2 'chrony 时间同步软件包已安装' 'chrony 软件包未安装' rpm -q chrony
check 4 'chrony 仅启用了 server controller iburst,默认外网源已注释' 'chrony 应仅启用 server controller iburst,并注释默认外网源' chrony_config
check 2 'chronyd 时间同步服务正在运行' 'chronyd 未运行' active_service chronyd
check 2 'chronyd 时间同步服务已设置持久开机自启' 'chronyd 未持久设置开机自启' enabled_service chronyd
check 5 'chronyd 已选中控制节点为同步源(^*),且未加载其他服务器源' 'chronyd 当前应选中控制节点为同步源(^*),且没有其他服务器源' chrony_synced
section 'Nova 软件包(5分)'
check 5 'openstack-nova-compute 软件包已安装' 'openstack-nova-compute 未安装' rpm -q openstack-nova-compute
section 'Nova 配置(30分)'
# 配置按小组评分;组内错误键附在失败提示中,不输出实际值或口令。
config_group() {
local points=$1 group=$2 key expected actual bad=''
shift 2
while [ "$#" -ge 2 ]; do
key=$1 expected=$2; shift 2
actual=$(ini_value "$CONF" "$group" "$key")
case "$key" in
enabled|use_neutron)
actual=$(printf '%s' "$actual" | tr '[:upper:]' '[:lower:]')
case "$actual" in 1|yes|on) actual=true;; esac;;
project_domain_name|user_domain_name)
actual=$(printf '%s' "$actual" | tr '[:upper:]' '[:lower:]')
expected=$(printf '%s' "$expected" | tr '[:upper:]' '[:lower:]');;
enabled_apis)
# APIs 的顺序无关;仅接受手册要求的这两个 API。
actual=$(ini_value "$CONF" "$group" "$key" | tr ',' '\n' | tr -d ' \t\r' | sort | tr '\n' ',')
expected='metadata,osapi_compute,';;
server_proxyclient_address)
[ "$actual" != '$my_ip' ] || actual=$(ini_value "$CONF" DEFAULT my_ip);;
auth_url|api_servers) actual=${actual%/};;
esac
if [ -z "$actual" ] || [ "$actual" != "$expected" ]; then bad="$bad $key"; fi
done
# my_ip 必须同时属于本机;即便 hosts 未完成,也不接受示例中的远端 IP。
if [ "$group" = DEFAULT ] && ! compute_mapping >/dev/null 2>&1; then bad="$bad my_ip/hosts/本机地址"; fi
check "$points" "[$group] Nova 配置符合实验要求" "[$group] 错误或缺失的配置键:${bad:-文件不可读};请检查 $CONF" test -z "$bad"
}
config_group 6 DEFAULT enabled_apis 'osapi_compute,metadata' transport_url 'rabbit://openstack:openstack@controller' my_ip "$COMPUTE_IP" use_neutron true firewall_driver nova.virt.firewall.NoopFirewallDriver
config_group 2 api auth_strategy keystone
config_group 6 keystone_authtoken auth_url http://controller:5000/v3 memcached_servers controller:11211 auth_type password project_domain_name Default user_domain_name Default project_name service username nova password nova
config_group 6 vnc enabled true server_listen 0.0.0.0 server_proxyclient_address "$COMPUTE_IP" novncproxy_base_url "http://$CONTROLLER_IP:6080/vnc_auto.html"
config_group 3 glance api_servers http://controller:9292
config_group 2 oslo_concurrency lock_path /var/lib/nova/tmp
config_group 5 placement region_name RegionOne project_domain_name Default project_name service auth_type password user_domain_name Default auth_url http://controller:5000/v3 username placement password placement
section '虚拟化类型(5分)'
virtualization_ok() {
local type
[ -r /proc/cpuinfo ] && [ -r "$CONF" ] || return 1
type=$(ini_value "$CONF" libvirt virt_type)
if grep -Eq '(^|[[:space:]])(vmx|svm)([[:space:]]|$)' /proc/cpuinfo; then
# Rocky 默认为 kvm;显式使用 qemu 也可运行手册的计算服务。
case "$type" in ''|kvm|qemu) return 0;; *) return 1;; esac
fi
[ "$type" = qemu ]
}
check 5 'Nova 虚拟化类型与 CPU 硬件虚拟化能力匹配' '无 vmx/svm 时必须设置 [libvirt] virt_type=qemu;有扩展时允许默认 kvm 或 qemu' virtualization_ok
section '计算服务运行与开机自启(10分)'
for service in libvirtd openstack-nova-compute; do
check 3 "$service 服务正在运行" "$service 未运行" active_service "$service"
check 2 "$service 服务已设置持久开机自启" "$service 未持久设置开机自启" enabled_service "$service"
done
finish