From b91d121a42e0e214459fc9554b98f37d7aaad98b Mon Sep 17 00:00:00 2001 From: seaHi Date: Fri, 9 Oct 2026 14:45:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20OpenStack=203-5=20?= =?UTF-8?q?=E6=8E=A7=E5=88=B6=E8=8A=82=E7=82=B9=E8=87=AA=E5=8A=A8=E5=8C=96?= =?UTF-8?q?=E8=AF=84=E5=88=86=E4=B8=8E=E8=87=AA=E6=B5=8B=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 3-5-controller.sh | 252 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100755 3-5-controller.sh diff --git a/3-5-controller.sh b/3-5-controller.sh new file mode 100755 index 0000000..d0b4e64 --- /dev/null +++ b/3-5-controller.sh @@ -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