Files
OpenStackScripts/3-5-controller.sh
T

260 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
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