feat: 添加 OpenStack 3-5 控制节点自动化评分与自测脚本
This commit is contained in:
1 parent
fc063cd56c
commit
b91d121a42
1 file changed
+252
Executable
+252
@@ -0,0 +1,252 @@
|
||||
#!/bin/bash
|
||||
# 3-5 控制节点评分:CentOS 7.9 / OpenStack Rocky,Bash 4+,满分 100。
|
||||
# 用法:在 controller 上执行 bash 3-5-controller.sh
|
||||
# 可选:CHECK_CONTROLLER_IP、CHECK_COMPUTE_IP(默认读取 hosts)
|
||||
# CHECK_ADMIN_PASSWORD(默认 admin)、CHECK_NOVA_PASSWORD(默认 nova)
|
||||
# 只读检查;不运行环境重置、discover_hosts、数据库同步或任何服务修改。
|
||||
# 使用实验管理员凭证直接查询,不 source 学生编写的 admin-openstack.sh。
|
||||
# 检查当前结果,无法证明学生执行过重置/加载环境变量/发现主机的命令。
|
||||
# 退出码:0=100分,1=有扣分,2=权限/基础工具/参数错误,未完成评分。
|
||||
# 分值:网络20、认证10、compute注册10、compute可用10、其他计算服务15、
|
||||
# Cell映射15、服务目录10、nova-status10。
|
||||
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
|
||||
# 自动检测失败(例如 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)}
|
||||
ADMIN_PASSWORD=${CHECK_ADMIN_PASSWORD-admin}
|
||||
NOVA_PASSWORD=${CHECK_NOVA_PASSWORD-nova}
|
||||
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
|
||||
|
||||
# 在子 Shell 内隔离已有 OS_* 变量;不改变调用者环境,不输出密码或 Token。
|
||||
os_cli() (
|
||||
for variable in ${!OS_@}; do unset "$variable"; done
|
||||
export OS_AUTH_URL=http://controller:5000/v3 OS_IDENTITY_API_VERSION=3
|
||||
export OS_AUTH_TYPE=password OS_USERNAME=admin OS_PASSWORD="$ADMIN_PASSWORD"
|
||||
export OS_PROJECT_NAME=admin OS_USER_DOMAIN_NAME=Default OS_PROJECT_DOMAIN_NAME=Default
|
||||
export OS_REGION_NAME=RegionOne
|
||||
export NO_PROXY="localhost,127.0.0.1,controller,compute,$CONTROLLER_IP,$COMPUTE_IP"
|
||||
export no_proxy="$NO_PROXY"
|
||||
timeout 30 openstack "$@" 2>/dev/null
|
||||
)
|
||||
|
||||
# JSON 按字段名读取,兼容不同版本客户端改变列表列顺序。
|
||||
# Nova/OpenStack 客户端已依赖 Python;兼容 CentOS 7 的 Python 2.7 和 Python 3。
|
||||
json_values() {
|
||||
local interpreter
|
||||
interpreter=$(command -v python3 || command -v python) || return 1
|
||||
"$interpreter" -c '
|
||||
import json, sys
|
||||
rows = json.load(sys.stdin)
|
||||
if not isinstance(rows, list):
|
||||
sys.exit(1)
|
||||
for row in rows:
|
||||
values = [str(row[key]) for key in sys.argv[1:]]
|
||||
if any(any(c in value for c in "\t\r\n") for value in values):
|
||||
sys.exit(1)
|
||||
sys.stdout.write("\t".join(values) + "\n")
|
||||
' "$@" 2>/dev/null
|
||||
}
|
||||
|
||||
section '控制节点主机与网络(20分)'
|
||||
hostname_ok() { [ "$(hostname)" = controller ] && [ "$(tr -d '[:space:]' < /etc/hostname)" = controller ]; }
|
||||
controller_mapping() {
|
||||
[ "$(hosts_ip controller)" = "$CONTROLLER_IP" ] &&
|
||||
ip -o -4 addr show scope global | awk -v ip="$CONTROLLER_IP" '{split($4,a,"/"); if(a[1]==ip)found=1} END{exit !found}'
|
||||
}
|
||||
compute_mapping() { valid_ipv4 "$COMPUTE_IP" && [ "$COMPUTE_IP" != "$CONTROLLER_IP" ] && [ "$(hosts_ip compute)" = "$COMPUTE_IP" ]; }
|
||||
both_resolve() { resolved_as controller "$CONTROLLER_IP" && resolved_as compute "$COMPUTE_IP"; }
|
||||
check 4 '运行中及持久主机名均为 controller' '运行中及持久主机名都应为 controller' hostname_ok
|
||||
check 4 'controller 的 hosts 映射唯一,且指向本机管理 IP' '/etc/hosts 中 controller 应唯一映射到本机管理 IP' controller_mapping
|
||||
check 4 'compute 的 hosts 映射唯一,管理 IPv4 配置正确' '/etc/hosts 中 compute 应唯一映射到计算节点管理 IPv4' compute_mapping
|
||||
check 4 'controller 和 compute 的系统解析与 hosts 配置一致' 'controller/compute 的系统解析应与 hosts 一致' both_resolve
|
||||
check 4 '可通过主机名 ping 通 compute' '应能通过主机名 ping 通 compute' timeout 8 ping -4 -c 2 -W 2 compute
|
||||
|
||||
section 'Keystone 管理员认证(10分)'
|
||||
AUTH_OK=0
|
||||
if os_cli token issue >/dev/null; then AUTH_OK=1; fi
|
||||
check 10 'Keystone 管理员认证成功,可获取 Token' '管理员认证失败,检查 Keystone 或 CHECK_ADMIN_PASSWORD(默认 admin)' test "$AUTH_OK" -eq 1
|
||||
|
||||
# 只认完整 Host=compute 的行,避免 controller 上克隆遗留的 nova-compute 冒充。
|
||||
# 使用 JSON 字段名,避免依赖表格边框或列位置。
|
||||
SERVICES_OK=0
|
||||
SERVICES=''
|
||||
if [ "$AUTH_OK" -eq 1 ]; then
|
||||
if SERVICES=$(os_cli compute service list -f json | json_values Binary Host Status State); then SERVICES_OK=1; fi
|
||||
fi
|
||||
compute_registered() {
|
||||
[ "$SERVICES_OK" -eq 1 ] && printf '%s\n' "$SERVICES" | awk '
|
||||
$1=="nova-compute" && $2=="compute" {n++}
|
||||
END{exit (n!=1)}'
|
||||
}
|
||||
compute_ready() {
|
||||
[ "$SERVICES_OK" -eq 1 ] && printf '%s\n' "$SERVICES" | awk '
|
||||
$1=="nova-compute" && $2=="compute" {n++; if($3=="enabled" && $4=="up")good++}
|
||||
END{exit !(n==1 && good==1)}'
|
||||
}
|
||||
section '计算节点注册(10分)'
|
||||
check 10 '计算服务已唯一注册,Host 为 compute,Binary 为 nova-compute' '应唯一注册 Host=compute 的 nova-compute(认证或服务列表查询失败也不得分)' compute_registered
|
||||
section '计算节点可用状态(10分)'
|
||||
check 10 'compute 的 nova-compute 状态为 enabled/up' 'compute 的 nova-compute 应为 enabled/up' compute_ready
|
||||
|
||||
section '所有计算服务状态(15分)'
|
||||
control_component() {
|
||||
[ "$SERVICES_OK" -eq 1 ] && printf '%s\n' "$SERVICES" | awk -v binary="$1" '
|
||||
$1==binary && $2=="controller" {n++; if($3=="enabled" && $4=="up")good++}
|
||||
END{exit !(n>=1 && n==good)}'
|
||||
}
|
||||
all_services_up() {
|
||||
[ "$SERVICES_OK" -eq 1 ] && printf '%s\n' "$SERVICES" | awk '
|
||||
NF {n++; if(NF!=4 || $3!="enabled" || $4!="up")bad=1}
|
||||
END{exit !(n>0 && !bad)}'
|
||||
}
|
||||
check 5 'controller 的 nova-scheduler 已注册,且全部为 enabled/up' 'controller 的 nova-scheduler 应存在且全部为 enabled/up' control_component nova-scheduler
|
||||
check 5 'controller 的 nova-conductor 已注册,且全部为 enabled/up' 'controller 的 nova-conductor 应存在且全部为 enabled/up' control_component nova-conductor
|
||||
check 5 '计算服务列表查询成功,所有服务均为 enabled/up' '计算服务列表不能为空,且所有列出的服务应为 enabled/up' all_services_up
|
||||
|
||||
section '计算节点 Cell 数据库映射(15分)'
|
||||
# discover_hosts 会写数据库,评分时仅查询 host_mappings,避免替学生完成实验。
|
||||
cell_mapping() {
|
||||
local result
|
||||
result=$(MYSQL_PWD="$NOVA_PASSWORD" timeout 15 mysql --connect-timeout=5 --protocol=TCP \
|
||||
-h controller -u nova -N -B nova_api -e \
|
||||
"SELECT c.uuid FROM host_mappings h JOIN cell_mappings c ON c.id=h.cell_id WHERE h.host='compute';" 2>/dev/null) || return 1
|
||||
printf '%s\n' "$result" | awk '
|
||||
/^[0-9a-fA-F-]+$/ && length($0)==36 && $0!="00000000-0000-0000-0000-000000000000" {good++}
|
||||
END{exit !(NR==1 && good==1)}'
|
||||
}
|
||||
check 15 'compute 已唯一映射到有效的非 cell0 Cell' 'compute 未唯一映射到有效的非 cell0 Cell,或 nova_api 数据库查询失败;检查 discover_hosts 结果及 nova 账号' cell_mapping
|
||||
|
||||
section 'Keystone 服务目录和 API 端点(10分)'
|
||||
CATALOG_OK=0
|
||||
if [ "$AUTH_OK" -eq 1 ] && os_cli catalog list >/dev/null; then CATALOG_OK=1; fi
|
||||
catalog_endpoints() {
|
||||
local type=$1 port=$2 endpoints
|
||||
[ "$CATALOG_OK" -eq 1 ] || return 1
|
||||
endpoints=$(os_cli endpoint list --service "$type" -f json | json_values Region Interface URL Enabled) || return 1
|
||||
printf '%s\n' "$endpoints" | awk -v port="$port" -v ip="$CONTROLLER_IP" '
|
||||
$1=="RegionOne" && ($4=="True" || $4=="true") {
|
||||
url=$3; sub(/\/$/,"",url)
|
||||
expected="http://controller:"port; other="http://"ip":"port
|
||||
if(port==8774){expected=expected"/v2.1"; other=other"/v2.1"}
|
||||
if(url==expected || url==other) seen[$2]=1
|
||||
}
|
||||
END{exit !(seen["public"] && seen["internal"] && seen["admin"])}'
|
||||
}
|
||||
check 5 '服务目录可查询,compute 的三个 RegionOne API 端点均配置正确' '服务目录查询失败,或 compute 在 RegionOne 的 public/internal/admin 端点不完整或 URL 错误' catalog_endpoints compute 8774
|
||||
check 5 '服务目录可查询,placement 的三个 RegionOne API 端点均配置正确' '服务目录查询失败,或 placement 在 RegionOne 的 public/internal/admin 端点不完整或 URL 错误' catalog_endpoints placement 8778
|
||||
|
||||
section 'Nova 整体检查(10分)'
|
||||
# Rocky 返回码:0=全部成功,1=警告,2=失败;仅 0 得分,命令缺失/超时也失败。
|
||||
check 10 'nova-status upgrade check 的所有检查项均成功' 'nova-status upgrade check 未全部成功(警告也扣分),请手工运行查看详细结果' timeout 40 su -s /bin/sh -c 'nova-status upgrade check' nova
|
||||
finish
|
||||
Reference in new issue
Block a user