Files
OpenStackScripts/3-5-controller.sh
T

253 lines
12 KiB
Bash
Executable File
Raw 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
# 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