260 lines
12 KiB
Bash
Executable File
260 lines
12 KiB
Bash
Executable File
#!/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
|
||
MANUAL_LOCATION=''
|
||
MANUAL_HINT_SHOWN=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))
|
||
MANUAL_LOCATION=${2:-}
|
||
MANUAL_HINT_SHOWN=0
|
||
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%b\n' "$GREEN" "$success_message" "$NC"
|
||
else
|
||
FAIL_CNT=$((FAIL_CNT+1))
|
||
printf '%b[✘ 失败] %s%b\n' "$RED" "$failure_message" "$NC"
|
||
# 同一任务只在第一次失败时提示手册位置;全部通过时不增加输出。
|
||
if [ -n "$MANUAL_LOCATION" ] && [ "$MANUAL_HINT_SHOWN" -eq 0 ]; then
|
||
printf '%b → 手册:%s%b\n' "$RED" "$MANUAL_LOCATION" "$NC"
|
||
MANUAL_HINT_SHOWN=1
|
||
fi
|
||
fi
|
||
}
|
||
finish() {
|
||
printf '\n'
|
||
separator
|
||
printf '%b总成绩:%d/100 分%b | 通过: %b%d%b | 失败: %b%d%b\n' "$YELLOW" "$SCORE" "$NC" "$GREEN" "$PASS_CNT" "$NC" "$RED" "$FAIL_CNT" "$NC"
|
||
if [ "$TOTAL" -ne 100 ]; then
|
||
error "评分项总分异常:$TOTAL,请联系教师检查脚本。"
|
||
separator
|
||
exit 2
|
||
fi
|
||
if [ "$FAIL_CNT" -eq 0 ]; then
|
||
separator
|
||
exit 0
|
||
fi
|
||
printf '%b请按红字提示修正。%b\n' "$RED" "$NC"
|
||
separator
|
||
exit 1
|
||
}
|
||
preflight() {
|
||
if [ "$EUID" -ne 0 ]; then error '请在 controller 节点用 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分)' '节点环境准备 / 三、配置主机名解析(在 controller 检查 /etc/hosts)'
|
||
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 映射' 'controller hosts 应指向本机管理 IP' controller_mapping
|
||
check 4 'compute hosts 映射' 'compute hosts 映射缺失或冲突' compute_mapping
|
||
check 4 '主机名解析' '主机名解析与 hosts 不一致' both_resolve
|
||
check 4 'ping compute' 'ping compute 不通' timeout 8 ping -4 -c 2 -W 2 compute
|
||
|
||
section '管理员认证(10分)' '部署过程 / 二、将计算节点添加到 Cell 数据库 / 第1步(检查管理员凭证)'
|
||
AUTH_OK=0
|
||
if os_cli token issue >/dev/null; then AUTH_OK=1; fi
|
||
check 10 '管理员认证' '管理员认证失败,检查 Keystone 和密码' 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 'compute 注册(10分)' '部署过程 / 二、将计算节点添加到 Cell 数据库 / 第2步'
|
||
check 10 'compute 服务注册' 'compute 未唯一注册或查询失败' compute_registered
|
||
section 'compute 状态(10分)' '部署过程 / 一、安装并配置 Nova 计算组件 / 启动计算服务(回到 compute 检查)'
|
||
check 10 'nova-compute enabled/up' 'nova-compute 未处于 enabled/up' compute_ready
|
||
|
||
section '服务状态(15分)' '部署过程 / 三、验证部署结果 / 第1步(控制节点异常时回查环境准备第一节)'
|
||
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 'nova-scheduler enabled/up' 'nova-scheduler 缺失或未处于 enabled/up' control_component nova-scheduler
|
||
check 5 'nova-conductor enabled/up' 'nova-conductor 缺失或未处于 enabled/up' control_component nova-conductor
|
||
check 5 '全部计算服务 enabled/up' '服务列表为空、查询失败或有异常服务' all_services_up
|
||
|
||
section 'Cell 映射(15分)' '部署过程 / 二、将计算节点添加到 Cell 数据库 / 第3步'
|
||
# 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 Cell 映射' 'compute 加入 Cell 异常,检查 discover_hosts 结果及数据库连接' cell_mapping
|
||
|
||
section '服务目录(10分)' '部署过程 / 三、验证部署结果 / 第2步'
|
||
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 服务目录与端点' 'compute 服务目录或端点异常' catalog_endpoints compute 8774
|
||
check 5 'placement 服务目录与端点' 'placement 服务目录或端点异常' catalog_endpoints placement 8778
|
||
|
||
section '部署检查(10分)' '部署过程 / 三、验证部署结果 / 第3步'
|
||
# Rocky 返回码:0=全部成功,1=警告,2=失败;仅 0 得分,命令缺失/超时也失败。
|
||
check 10 'nova-status upgrade check' 'nova-status 检查未全部成功' timeout 40 su -s /bin/sh -c 'nova-status upgrade check' nova
|
||
finish
|