Files
OpenStackScripts/3.4.sh
T

414 lines
18 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
# OpenStack 控制节点(CentOS 7.9 / Rocky)Nova 计算服务部署检测
# 参考 3.2.sh、3.3.sh;按手册章节汇总,异常提示对应章内步骤。
# 用法:以 root 执行 bash 3.4.sh。仅检查,不安装、同步数据库或重启服务。
# 默认实验密码:数据库 root=123456,管理员 admin=admin。
# 可通过 CHECK_DB_ROOT_PASSWORD、CHECK_ADMIN_PASSWORD 覆盖以上密码。
# 退出码:0=全部通过,1=有失败,2=无失败但存在未验证项。
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
set -o pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
CYAN='\033[0;36m'
NC='\033[0m'
PASS_CNT=0
FAIL_CNT=0
SKIP_CNT=0
SECTION_TITLE=''
SECTION_PASS=0
SECTION_FAIL=0
SECTION_SKIP=0
SECTION_DETAILS=()
# 正常检查仅计数;每章输出一行汇总,异常才展开具体步骤。
pass() { PASS_CNT=$((PASS_CNT+1)); }
detail() {
if [ -n "$SECTION_TITLE" ]; then
SECTION_DETAILS+=("$1")
else
printf '%s\n' "$1"
fi
}
fail() { FAIL_CNT=$((FAIL_CNT+1)); detail " [✘ 失败] $1"; }
skip() { SKIP_CNT=$((SKIP_CNT+1)); detail " [− 跳过] $1"; }
section_summary() {
[ -n "$SECTION_TITLE" ] || return 0
local passed=$((PASS_CNT-SECTION_PASS)) failed=$((FAIL_CNT-SECTION_FAIL)) skipped=$((SKIP_CNT-SECTION_SKIP))
if [ "$failed" -gt 0 ]; then
printf '%b[✘] %s:通过 %s,失败 %s,跳过 %s%b\n' "$RED" "$SECTION_TITLE" "$passed" "$failed" "$skipped" "$NC"
elif [ "$skipped" -gt 0 ]; then
printf '%b[−] %s:通过 %s,跳过 %s%b\n' "$YELLOW" "$SECTION_TITLE" "$passed" "$skipped" "$NC"
else
printf '%b[✔] %s:%s 项通过%b\n' "$GREEN" "$SECTION_TITLE" "$passed" "$NC"
fi
if [ "${#SECTION_DETAILS[@]}" -gt 0 ]; then
printf '%s\n' "${SECTION_DETAILS[@]}"
fi
}
info() {
section_summary
SECTION_TITLE=$1
SECTION_PASS=$PASS_CNT
SECTION_FAIL=$FAIL_CNT
SECTION_SKIP=$SKIP_CNT
SECTION_DETAILS=()
}
printf '%bNova 控制节点部署检测%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 "[准备工作第 3 步 / 第三部分第 1 步] 缺少命令 ${cmd},请检查实验环境及软件包安装;相关检查可能失败或跳过。"
fi
done
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 "[第 2 步] [$section] $key 配置正确"
else
# 不回显实际值,避免泄露连接串或口令。
fail "[第 2 步] [$section] $key 缺失或与手册不符,请检查 $CONF"
fi
}
info '一、数据库与授权'
ROOT_OK=0
if db_root 'SELECT 1;' >/dev/null; then
ROOT_OK=1
pass '[第 1 步] 数据库 root 可登录'
else
fail '[第 1 步] 数据库 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 "[第 2 步] $database 数据库已创建"
else
fail "[第 2 步] $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 "[第 3 步] '$user'@'%' 已获得 $database 的所有权限"
else
fail "[第 3 步] '$user'@'%' 缺少 $database 的 ALL PRIVILEGES 授权,或授权查询失败"
fi
else
skip "[第 2、3 步] 无法独立核查 $database 是否创建及 '%' 账号授权;继续测试实际连接"
fi
if db_query "$database" 'SELECT 1;' >/dev/null; then
pass "[第 3 步] 使用手册服务账号经 controller 的 TCP 连接可访问 $database"
else
fail "[第 2、3 步] 无法访问 ${database},请检查建库、账号密码、授权及 controller 解析"
fi
done
info '二、服务凭证与端点'
AUTH_OK=0
if os_cli token issue >/dev/null; then
AUTH_OK=1
pass '[第 1 步] 管理员凭证可获取 Token'
else
fail '[第 1 步] 管理员认证失败,请检查 Keystone、admin 密码及 admin-openstack.sh;继续检查本地配置'
fi
check_identity() {
local user=$1 type=$2 user_step=$3 role_step=$4 service_step=$5 endpoint_step=$6 url=$7
local user_id service_id service_type roles endpoints interface
if [ "$AUTH_OK" -ne 1 ]; then
skip "[第 ${user_step}、${role_step}、${service_step}、${endpoint_step} 步] 管理员无法认证,暂不检查 $user 用户、角色、服务及端点"
return
fi
user_id=$(os_cli user show --domain default "$user" -f value -c id)
if [ -n "$user_id" ]; then
pass "[第 ${user_step} 步] default 域内的 $user 用户已创建"
# 用实际服务密码签发 service 项目的令牌,同时确认账号可用。
if os_cli --os-username "$user" --os-password "$user" --os-project-name service token issue >/dev/null; then
pass "[第 ${user_step}、${role_step} 步] $user 可使用手册密码在 service 项目完成认证"
else
fail "[第 ${user_step}、${role_step} 步] $user 认证失败,请检查密码、启用状态及 service 项目角色"
fi
if roles=$(os_cli role assignment list --user "$user_id" --project service --role admin -f value) && [ -n "$roles" ]; then
pass "[第 ${role_step} 步] $user 已绑定 service 项目的 admin 角色"
else
fail "[第 ${role_step} 步] $user 未绑定 service 项目的 admin 角色,或查询失败"
fi
else
fail "[第 ${user_step} 步] 未找到 default 域的 $user 用户,或查询失败"
skip "[第 ${role_step} 步] 无法确认 $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 "[第 ${service_step} 步] $user 服务实体存在,类型为 $type"
else
fail "[第 ${service_step} 步] $user 服务实体不存在、重名或类型不是 $type"
skip "[第 ${endpoint_step} 步] 服务实体未确认,暂不检查端点"
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 "[第 ${endpoint_step} 步] $user 的 RegionOne/$interface 端点为 $url"
else
fail "[第 ${endpoint_step} 步] $user 的 RegionOne/$interface 端点缺失、重复、未启用或 URL 错误,应为 $url"
fi
done
}
check_identity nova compute 2 3 4 5 http://controller:8774/v2.1
check_identity placement placement 6 7 8 9 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 "[第 1 步] $package 已安装"
else
fail "[第 1 步] $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 '[第 2 步] [DEFAULT] my_ip 是本机已配置的 IPv4 地址'
else
fail '[第 2 步] [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 "[第 2 步] [vnc] $key 引用 my_ip 或使用相同地址"
else
fail "[第 2 步] [vnc] $key 应引用 \$my_ip 或填写相同的管理 IP"
fi
done
else
fail "[第 2 步] $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 '[第 3 步] /usr/bin 的 Directory 块包含手册要求的访问授权指令'
else
fail "[第 3 步] $APACHE_CONF 缺少手册中的 /usr/bin Directory 授权配置"
fi
if timeout 15 httpd -t >/dev/null 2>&1; then
pass '[第 3、4 步] Apache 配置语法检查通过'
else
fail '[第 3、4 步] Apache 配置语法检查失败,请执行 httpd -t 查看具体错误'
fi
if systemctl is-active --quiet httpd; then
pass '[第 4 步] httpd 正在运行'
else
fail '[第 4 步] 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 '[第 3、4 步] Placement API 根路径返回 HTTP 200';;
401) skip '[第 3、4 步] Placement API 返回 HTTP 401,仅确认服务可达,未验证认证后的访问';;
*) fail "[第 3、4 步] Placement API 返回 HTTP ${http_code:-000},请检查授权配置、httpd 日志及 8778 端口";;
esac
info '四、数据库同步与 Cells'
check_tables() {
local database=$1 step=$2 tables table missing=''
shift 2
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 "[第 ${step}、6 步] $database 已生成关键数据表"
else
fail "[第 ${step}、6 步] $database 缺少表:${missing},请检查该步骤数据库同步是否成功"
fi
else
skip "[第 ${step}、6 步] 无法连接 ${database},先完成第一部分第 2、3 步,再验证数据表"
fi
}
check_tables nova_api 1 cell_mappings instance_mappings
check_tables placement 1 resource_providers inventories allocations
check_tables nova_cell0 4 instances instance_extra
check_tables nova 4 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
step=2
expected_db=mysql+pymysql://nova:nova@controller/nova_cell0
expected_transport=none:///
else
step=3
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 "[第 ${step}、5 步] $cell 唯一存在,数据库及消息队列映射正确"
else
fail "[第 ${step}、5 步] $cell 缺失、重复或映射错误,请检查 list_cells(不要盲目重复创建 cell1)"
fi
done
else
skip '[第 2、3、5 步] 无法读取 cell_mappings,请先检查数据库权限及第 1 步的同步结果'
fi
if timeout 30 su -s /bin/sh -c 'nova-manage cell_v2 list_cells' nova >/dev/null 2>&1; then
pass '[第 5 步] nova 用户可成功执行 nova-manage cell_v2 list_cells'
else
fail '[第 5 步] 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"; then
pass "[第 1、3 步] $service 正在运行"
else
fail "[第 1、3 步] $service 未正常运行"
fi
if [ "$(systemctl is-enabled "$service" 2>/dev/null)" = enabled ]; then
pass "[第 2 步] $service 已设置开机自启"
else
fail "[第 2 步] $service 未设置持久的开机自启"
fi
done
info '结果自测:Compute API'
if [ "$AUTH_OK" -eq 1 ]; then
if os_cli compute service list >/dev/null; then
pass '管理员可查询计算服务列表'
else
fail '[第二部分第 5 步、第三部分第 2 步、第五部分第 3 步] 计算服务列表查询失败,请检查 API、端点和认证配置'
fi
else
skip '[第二部分第 1 步] 管理员认证失败,暂不验证 Compute API'
fi
section_summary
printf '\n合计:通过 %b%s%b,失败 %b%s%b,跳过 %b%s%b\n' \
"$GREEN" "$PASS_CNT" "$NC" "$RED" "$FAIL_CNT" "$NC" "$YELLOW" "$SKIP_CNT" "$NC"
if [ "$FAIL_CNT" -gt 0 ]; then
printf '%b请按异常项的手册编号修正后重试。%b\n' "$RED" "$NC"
exit 1
elif [ "$SKIP_CNT" -gt 0 ]; then
printf '%b存在未验证项,请解决跳过原因后重试。%b\n' "$YELLOW" "$NC"
exit 2
else
exit 0
fi