401 lines
16 KiB
Bash
Executable File
401 lines
16 KiB
Bash
Executable File
#!/bin/bash
|
||
# OpenStack 控制节点(CentOS 7.9 / Rocky)Nova 计算服务部署检测
|
||
# 用法:以 root 执行 bash 3.4.sh。仅检查,不安装、同步数据库或重启服务。
|
||
# 默认实验密码:数据库 root=123456,管理员 admin=admin。
|
||
# 可通过 CHECK_DB_ROOT_PASSWORD、CHECK_ADMIN_PASSWORD 覆盖以上密码。
|
||
# 退出码:0=本次检查项全部通过,1=有失败。
|
||
|
||
export LANG=en_US.UTF-8
|
||
export LC_ALL=en_US.UTF-8
|
||
set -o pipefail
|
||
|
||
# 学生可见输出:每组一行,失败时列出原因;仅统计实际检查结果。
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
CYAN='\033[0;36m'
|
||
NC='\033[0m'
|
||
PASS_CNT=0
|
||
FAIL_CNT=0
|
||
SECTION_TITLE=''
|
||
SECTION_PASS=0
|
||
SECTION_FAIL=0
|
||
SECTION_DETAILS=()
|
||
|
||
pass() { PASS_CNT=$((PASS_CNT+1)); }
|
||
fail() {
|
||
FAIL_CNT=$((FAIL_CNT+1))
|
||
if [ -n "$SECTION_TITLE" ]; then
|
||
SECTION_DETAILS+=("$1")
|
||
else
|
||
printf '%b[✘ 失败] %s%b\n' "$RED" "$1" "$NC"
|
||
fi
|
||
}
|
||
section_summary() {
|
||
[ -n "$SECTION_TITLE" ] || return 0
|
||
local passed=$((PASS_CNT-SECTION_PASS)) failed=$((FAIL_CNT-SECTION_FAIL)) message
|
||
# 没有实际检查结果的分组不显示,也不记为通过。
|
||
if [ "$failed" -gt 0 ]; then
|
||
printf '%b[✘ 失败] %s:通过 %s,失败 %s%b\n' "$RED" "$SECTION_TITLE" "$passed" "$failed" "$NC"
|
||
for message in "${SECTION_DETAILS[@]}"; do
|
||
printf '%b · %s%b\n' "$RED" "$message" "$NC"
|
||
done
|
||
elif [ "$passed" -gt 0 ]; then
|
||
printf '%b[✔ 通过] %s(%s 项)%b\n' "$GREEN" "$SECTION_TITLE" "$passed" "$NC"
|
||
fi
|
||
}
|
||
info() {
|
||
section_summary
|
||
SECTION_TITLE=$1
|
||
SECTION_PASS=$PASS_CNT
|
||
SECTION_FAIL=$FAIL_CNT
|
||
SECTION_DETAILS=()
|
||
}
|
||
finish() {
|
||
section_summary
|
||
printf '\n检查完毕:通过 %b%s%b 项,失败 %b%s%b 项。\n' \
|
||
"$GREEN" "$PASS_CNT" "$NC" "$RED" "$FAIL_CNT" "$NC"
|
||
if [ "$FAIL_CNT" -gt 0 ]; then
|
||
printf '%b请按失败提示修正后重试。%b\n' "$RED" "$NC"
|
||
exit 1
|
||
fi
|
||
printf '%b本次检查项全部通过。%b\n' "$GREEN" "$NC"
|
||
exit 0
|
||
}
|
||
|
||
printf '%bOpenStack Nova 控制节点自测%b\n' "$CYAN" "$NC"
|
||
|
||
if [ "$EUID" -ne 0 ]; then
|
||
fail '请在控制节点上使用 root 权限执行此脚本。'
|
||
exit 1
|
||
fi
|
||
|
||
CONF=/etc/nova/nova.conf
|
||
APACHE_CONF=/etc/httpd/conf.d/00-nova-placement-api.conf
|
||
DB_ROOT_PASSWORD=${CHECK_DB_ROOT_PASSWORD-123456}
|
||
ADMIN_PASSWORD=${CHECK_ADMIN_PASSWORD-admin}
|
||
|
||
# 远程查询均设置超时,避免服务不可达时一直等待。
|
||
for cmd in timeout mysql openstack rpm ip systemctl httpd nova-manage curl; do
|
||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||
fail "缺少命令 ${cmd},请安装对应软件包"
|
||
fi
|
||
done
|
||
|
||
[ "$FAIL_CNT" -eq 0 ] || finish
|
||
|
||
db_root() {
|
||
MYSQL_PWD="$DB_ROOT_PASSWORD" timeout 15 mysql --connect-timeout=5 \
|
||
-uroot -N -B -e "$1" 2>/dev/null
|
||
}
|
||
db_query() {
|
||
local database=$1 user=nova password=nova
|
||
[ "$database" != placement ] || { user=placement; password=placement; }
|
||
MYSQL_PWD="$password" timeout 15 mysql --connect-timeout=5 --protocol=TCP \
|
||
-h controller -u "$user" -N -B "$database" -e "$2" 2>/dev/null
|
||
}
|
||
|
||
# 隔离调用者旧的 OS_TOKEN、OS_CLOUD、OS_*_ID 等变量,避免它们覆盖本次凭证。
|
||
# 与参考脚本一样使用实验管理员凭证,不执行凭证文件里的任意命令。
|
||
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 NO_PROXY=localhost,127.0.0.1,controller no_proxy=localhost,127.0.0.1,controller
|
||
timeout 30 openstack "$@" 2>/dev/null
|
||
)
|
||
|
||
# 按节读取最后一个有效值,避免注释、其他节中的同名键造成误判。
|
||
ini_value() {
|
||
awk -v section="$1" -v key="$2" '
|
||
/^[[: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 }
|
||
' "$CONF" 2>/dev/null
|
||
}
|
||
check_config() {
|
||
local section=$1 key=$2 expected=$3 actual
|
||
actual=$(ini_value "$section" "$key")
|
||
case "$key" in
|
||
enabled|use_neutron|project_domain_name|user_domain_name)
|
||
actual=$(printf '%s' "$actual" | tr '[:upper:]' '[:lower:]')
|
||
expected=$(printf '%s' "$expected" | tr '[:upper:]' '[:lower:]');;
|
||
enabled_apis) actual=$(printf '%s' "$actual" | tr -d '[:space:]');;
|
||
esac
|
||
if [ "$actual" = "$expected" ]; then
|
||
pass "[$section] $key 配置正确"
|
||
else
|
||
# 不回显实际值,避免泄露连接串或口令。
|
||
fail "[$section] $key 缺失或与手册不符,请检查 $CONF"
|
||
fi
|
||
}
|
||
|
||
info '数据库与授权'
|
||
ROOT_OK=0
|
||
if db_root 'SELECT 1;' >/dev/null; then
|
||
ROOT_OK=1
|
||
pass '数据库 root 可登录'
|
||
else
|
||
fail '数据库 root 登录失败,请检查 MariaDB、密码(默认 123456)或 CHECK_DB_ROOT_PASSWORD'
|
||
fi
|
||
for database in nova_api nova nova_cell0 placement; do
|
||
if [ "$ROOT_OK" -eq 1 ]; then
|
||
if [ "$(db_root "SELECT COUNT(*) FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='$database';")" = 1 ]; then
|
||
pass "$database 数据库已创建"
|
||
else
|
||
fail "$database 数据库不存在或查询失败"
|
||
fi
|
||
user=nova
|
||
[ "$database" != placement ] || user=placement
|
||
grants=$(db_root "SHOW GRANTS FOR '$user'@'%';")
|
||
if printf '%s\n' "$grants" | tr -d '\140' | grep -Fq "GRANT ALL PRIVILEGES ON $database.* TO "; then
|
||
pass "'$user'@'%' 已获得 $database 的所有权限"
|
||
else
|
||
fail "'$user'@'%' 缺少 $database 的 ALL PRIVILEGES 授权,或授权查询失败"
|
||
fi
|
||
fi
|
||
if db_query "$database" 'SELECT 1;' >/dev/null; then
|
||
pass "使用手册服务账号经 controller 的 TCP 连接可访问 $database"
|
||
else
|
||
fail "无法访问 ${database},请检查建库、账号密码、授权及 controller 解析"
|
||
fi
|
||
done
|
||
|
||
info '服务凭证与端点'
|
||
AUTH_OK=0
|
||
if os_cli token issue >/dev/null; then
|
||
AUTH_OK=1
|
||
pass '管理员凭证可获取 Token'
|
||
else
|
||
fail '管理员认证失败,请检查 Keystone 服务及 admin 密码'
|
||
fi
|
||
|
||
check_identity() {
|
||
local user=$1 type=$2 url=$3
|
||
local user_id service_id service_type roles endpoints interface
|
||
if [ "$AUTH_OK" -ne 1 ]; then
|
||
return
|
||
fi
|
||
user_id=$(os_cli user show --domain default "$user" -f value -c id)
|
||
if [ -n "$user_id" ]; then
|
||
pass "default 域内的 $user 用户已创建"
|
||
# 用实际服务密码签发 service 项目的令牌,同时确认账号可用。
|
||
if os_cli --os-username "$user" --os-password "$user" --os-project-name service token issue >/dev/null; then
|
||
pass "$user 可使用手册密码在 service 项目完成认证"
|
||
else
|
||
fail "$user 认证失败,请检查密码、启用状态及 service 项目角色"
|
||
fi
|
||
if roles=$(os_cli role assignment list --user "$user_id" --project service --role admin -f value) && [ -n "$roles" ]; then
|
||
pass "$user 已绑定 service 项目的 admin 角色"
|
||
else
|
||
fail "$user 未绑定 service 项目的 admin 角色,或查询失败"
|
||
fi
|
||
else
|
||
fail "未找到 default 域的 $user 用户,或查询失败"
|
||
fi
|
||
service_id=$(os_cli service show "$user" -f value -c id)
|
||
service_type=$(os_cli service show "$user" -f value -c type)
|
||
if [ -n "$service_id" ] && [ "$service_type" = "$type" ]; then
|
||
pass "$user 服务实体存在,类型为 $type"
|
||
else
|
||
fail "$user 服务实体不存在、重名或类型不是 $type"
|
||
return
|
||
fi
|
||
for interface in public internal admin; do
|
||
if endpoints=$(os_cli endpoint list --service "$service_id" --region RegionOne \
|
||
--interface "$interface" -f value -c Enabled -c URL) && \
|
||
printf '%s\n' "$endpoints" | awk -v url="$url" '
|
||
NF { count++; sub(/\/$/, "", $2)
|
||
if (NF == 2 && tolower($1) == "true" && $2 == url) valid++ }
|
||
END { exit !(count == 1 && valid == 1) }
|
||
'; then
|
||
pass "$user 的 RegionOne/$interface 端点为 $url"
|
||
else
|
||
fail "$user 的 RegionOne/$interface 端点缺失、重复、未启用或 URL 错误,应为 $url"
|
||
fi
|
||
done
|
||
}
|
||
check_identity nova compute http://controller:8774/v2.1
|
||
check_identity placement placement http://controller:8778
|
||
|
||
info '软件包与配置'
|
||
for package in openstack-nova-api openstack-nova-conductor openstack-nova-novncproxy \
|
||
openstack-nova-scheduler openstack-nova-placement-api; do
|
||
if rpm -q "$package" >/dev/null 2>&1; then
|
||
pass "$package 已安装"
|
||
else
|
||
fail "$package 未安装"
|
||
fi
|
||
done
|
||
if [ -r "$CONF" ]; then
|
||
while IFS='|' read -r section key expected; do
|
||
check_config "$section" "$key" "$expected"
|
||
done <<'CONFIG'
|
||
DEFAULT|use_neutron|true
|
||
DEFAULT|firewall_driver|nova.virt.firewall.NoopFirewallDriver
|
||
DEFAULT|enabled_apis|osapi_compute,metadata
|
||
DEFAULT|transport_url|rabbit://openstack:openstack@controller
|
||
api_database|connection|mysql+pymysql://nova:nova@controller/nova_api
|
||
database|connection|mysql+pymysql://nova:nova@controller/nova
|
||
placement_database|connection|mysql+pymysql://placement:placement@controller/placement
|
||
api|auth_strategy|keystone
|
||
keystone_authtoken|auth_url|http://controller:5000/v3
|
||
keystone_authtoken|memcached_servers|controller:11211
|
||
keystone_authtoken|auth_type|password
|
||
keystone_authtoken|project_domain_name|Default
|
||
keystone_authtoken|user_domain_name|Default
|
||
keystone_authtoken|project_name|service
|
||
keystone_authtoken|username|nova
|
||
keystone_authtoken|password|nova
|
||
vnc|enabled|true
|
||
glance|api_servers|http://controller:9292
|
||
oslo_concurrency|lock_path|/var/lib/nova/tmp
|
||
placement|region_name|RegionOne
|
||
placement|project_domain_name|Default
|
||
placement|project_name|service
|
||
placement|auth_type|password
|
||
placement|user_domain_name|Default
|
||
placement|auth_url|http://controller:5000/v3
|
||
placement|username|placement
|
||
placement|password|placement
|
||
CONFIG
|
||
my_ip=$(ini_value DEFAULT my_ip)
|
||
if [ -n "$my_ip" ] && ip -o -4 addr show scope global 2>/dev/null | \
|
||
awk '{split($4,a,"/"); print a[1]}' | grep -Fxq "$my_ip"; then
|
||
pass '[DEFAULT] my_ip 是本机已配置的 IPv4 地址'
|
||
else
|
||
fail '[DEFAULT] my_ip 为空或不是本机 IPv4 地址,应填写自己的控制节点管理 IP'
|
||
fi
|
||
for key in server_listen server_proxyclient_address; do
|
||
value=$(ini_value vnc "$key")
|
||
if [ -n "$my_ip" ] && { [ "$value" = '$my_ip' ] || [ "$value" = "\${my_ip}" ] || [ "$value" = "$my_ip" ]; }; then
|
||
pass "[vnc] $key 引用 my_ip 或使用相同地址"
|
||
else
|
||
fail "[vnc] $key 应引用 \$my_ip 或填写相同的管理 IP"
|
||
fi
|
||
done
|
||
else
|
||
fail "$CONF 不存在或不可读"
|
||
fi
|
||
|
||
# 仅在指定 Directory 块内检查,注释及其他目录的授权不能算作通过。
|
||
if [ -r "$APACHE_CONF" ] && awk '
|
||
/^[[:space:]]*#/ { next }
|
||
{ line=tolower($0); gsub(/^[[:space:]]+|[[:space:]]+$/,"",line) }
|
||
line ~ /^<directory[[:space:]]+"?\/usr\/bin"?[[:space:]]*>$/ { inside=1; req=ord=allow=0; next }
|
||
inside && line == "require all granted" { req=1 }
|
||
inside && line == "order allow,deny" { ord=1 }
|
||
inside && line == "allow from all" { allow=1 }
|
||
inside && line == "</directory>" { if(req && ord && allow) found=1; inside=0 }
|
||
END { exit !found }
|
||
' "$APACHE_CONF"; then
|
||
pass '/usr/bin 的 Directory 块包含手册要求的访问授权指令'
|
||
else
|
||
fail "$APACHE_CONF 缺少手册中的 /usr/bin Directory 授权配置"
|
||
fi
|
||
if timeout 15 httpd -t >/dev/null 2>&1; then
|
||
pass 'Apache 配置语法检查通过'
|
||
else
|
||
fail 'Apache 配置语法检查失败,请执行 httpd -t 查看具体错误'
|
||
fi
|
||
if systemctl is-active --quiet httpd 2>/dev/null; then
|
||
pass 'httpd 正在运行'
|
||
else
|
||
fail 'httpd 未运行,请检查配置并按手册重启'
|
||
fi
|
||
http_code=$(curl --noproxy '*' -s -o /dev/null -w '%{http_code}' \
|
||
--connect-timeout 5 --max-time 10 http://controller:8778/ 2>/dev/null)
|
||
case "$http_code" in
|
||
200) pass 'Placement API 根路径返回 HTTP 200';;
|
||
401) : ;; # 认证要求不能证明接口功能正常,此项不计分。
|
||
*) fail "Placement API 返回 HTTP ${http_code:-000},请检查授权配置、httpd 日志及 8778 端口";;
|
||
esac
|
||
|
||
info '数据库同步与 Cells'
|
||
check_tables() {
|
||
local database=$1 tables table missing=''
|
||
shift
|
||
if tables=$(db_query "$database" 'SHOW TABLES;'); then
|
||
for table in "$@"; do
|
||
if ! printf '%s\n' "$tables" | grep -Fxq "$table"; then missing="$missing $table"; fi
|
||
done
|
||
if [ -z "$missing" ]; then
|
||
pass "$database 已生成关键数据表"
|
||
else
|
||
fail "$database 缺少表:${missing},请检查数据库同步"
|
||
fi
|
||
else
|
||
fail "$database 数据表查询失败,请检查连接和权限"
|
||
fi
|
||
}
|
||
check_tables nova_api cell_mappings instance_mappings
|
||
check_tables placement resource_providers inventories allocations
|
||
check_tables nova_cell0 instances instance_extra
|
||
check_tables nova instances compute_nodes services
|
||
|
||
# 查询数据库中的实际映射;不执行 map_cell0/create_cell/db sync 等写操作。
|
||
if cells=$(db_query nova_api 'SELECT name,uuid,transport_url,database_connection FROM cell_mappings;'); then
|
||
for cell in cell0 cell1; do
|
||
if [ "$cell" = cell0 ]; then
|
||
expected_db=mysql+pymysql://nova:nova@controller/nova_cell0
|
||
expected_transport=none:///
|
||
else
|
||
expected_db=mysql+pymysql://nova:nova@controller/nova
|
||
expected_transport=$(ini_value DEFAULT transport_url)
|
||
fi
|
||
if printf '%s\n' "$cells" | awk -F '\t' -v name="$cell" -v db="$expected_db" -v transport="$expected_transport" '
|
||
$1 == name { count++; if($4 == db && $3 == transport && $2 != "" &&
|
||
(name != "cell0" || $2 == "00000000-0000-0000-0000-000000000000")) valid++ }
|
||
END { exit !(count == 1 && valid == 1) }
|
||
'; then
|
||
pass "$cell 唯一存在,数据库及消息队列映射正确"
|
||
else
|
||
fail "$cell 缺失、重复或映射错误,请检查 nova-manage cell_v2 list_cells"
|
||
fi
|
||
done
|
||
else
|
||
fail 'cell_mappings 查询失败,请检查数据库权限和同步结果'
|
||
fi
|
||
if timeout 30 su -s /bin/sh -c 'nova-manage cell_v2 list_cells' nova >/dev/null 2>&1; then
|
||
pass 'nova 用户可成功执行 nova-manage cell_v2 list_cells'
|
||
else
|
||
fail 'nova 用户执行 list_cells 失败,请检查 nova.conf、文件权限及数据库连接'
|
||
fi
|
||
|
||
info '服务运行与自启'
|
||
for service in openstack-nova-api openstack-nova-scheduler openstack-nova-conductor openstack-nova-novncproxy; do
|
||
if systemctl is-active --quiet "$service" 2>/dev/null; then
|
||
pass "$service 正在运行"
|
||
else
|
||
fail "$service 未正常运行"
|
||
fi
|
||
if [ "$(systemctl is-enabled "$service" 2>/dev/null)" = enabled ]; then
|
||
pass "$service 已设置开机自启"
|
||
else
|
||
fail "$service 未设置持久的开机自启"
|
||
fi
|
||
done
|
||
|
||
info '结果自测:Compute API'
|
||
if [ "$AUTH_OK" -eq 1 ]; then
|
||
if os_cli compute service list >/dev/null; then
|
||
pass '管理员可查询计算服务列表'
|
||
else
|
||
fail '计算服务列表查询失败,请检查 API、端点和认证配置'
|
||
fi
|
||
fi
|
||
|
||
finish
|