From a376b574bb2b4585b4b4a34e1ca9728c3ad757b1 Mon Sep 17 00:00:00 2001 From: seaHi Date: Tue, 29 Sep 2026 20:54:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20OpenStack=20?= =?UTF-8?q?=E5=9F=BA=E7=A1=80=E8=99=9A=E6=8B=9F=E5=8C=96=E7=8E=AF=E5=A2=83?= =?UTF-8?q?=E5=92=8C=E6=8E=A7=E5=88=B6=E8=8A=82=E7=82=B9=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=8C=96=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 3.2.sh | 128 +++++++++ 3.3.sh | 139 ++++++++++ 3.4.sh | 413 ++++++++++++++++++++++++++++ env2-2.sh | 116 ++++++++ env3-1-local.sh | 307 +++++++++++++++++++++ env3-1.sh | 304 +++++++++++++++++++++ env3-2-local.sh | 353 ++++++++++++++++++++++++ env3-3-local.sh | 517 +++++++++++++++++++++++++++++++++++ env3-4-local.sh | 707 ++++++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 2984 insertions(+) create mode 100755 3.2.sh create mode 100755 3.3.sh create mode 100755 3.4.sh create mode 100755 env2-2.sh create mode 100755 env3-1-local.sh create mode 100755 env3-1.sh create mode 100755 env3-2-local.sh create mode 100755 env3-3-local.sh create mode 100755 env3-4-local.sh diff --git a/3.2.sh b/3.2.sh new file mode 100755 index 0000000..29dc62e --- /dev/null +++ b/3.2.sh @@ -0,0 +1,128 @@ +#!/bin/bash +# OpenStack 控制节点(Rocky) KeyStone 服务部署检查脚本 +# 适用于: 3.2 KeyStone服务部署 + +# 颜色定义 +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 + +pass() { echo -e "${GREEN}[✔ 通过] $1${NC}"; ((PASS_CNT++)); } +fail() { echo -e "${RED}[✘ 失败] $1${NC}"; ((FAIL_CNT++)); } +info() { echo -e "${CYAN}[➜] $1${NC}"; } + +echo -e "${YELLOW}====================================================${NC}" +echo -e "${YELLOW} OpenStack KeyStone 身份认证部署自测脚本 ${NC}" +echo -e "${YELLOW}====================================================${NC}" + +# 0. 检查是否为 root 用户 +if [ "$EUID" -ne 0 ]; then + echo -e "${RED}请使用 root 权限执行此脚本!${NC}" + exit 1 +fi + +# 1. 检查数据库配置与授权 +info "1. 检查 MariaDB 数据库创建与授权" +if mysql -ukeystone -pkeystone -e "SHOW DATABASES;" 2>/dev/null | grep -q "keystone"; then + pass "keystone 数据库已创建" +else + fail "keystone 数据库未创建" +fi + +if mysql -ukeystone -pkeystone -e "SHOW DATABASES;" &>/dev/null; then + pass "keystone 数据库用户授权正确 (可正常登录)" + + # 顺便检查 DB_SYNC 是否成功 + if [ $(mysql -ukeystone -pkeystone -e "USE keystone; SHOW TABLES;" 2>/dev/null | wc -l) -gt 2 ]; then + pass "keystone 数据库表结构已成功同步 (db_sync)" + else + fail "keystone 数据库表为空,未执行同步或同步失败" + fi +else + fail "keystone 数据库用户授权失败或密码不匹配" +fi + +# 2. 检查软件包安装 +info "2. 检查依赖软件包安装" +if rpm -q openstack-keystone httpd mod_wsgi &>/dev/null; then + pass "相关软件包 (keystone, httpd, mod_wsgi) 已全部安装" +else + fail "部分软件包未安装,请检查是否遗漏" +fi + +# 3. 检查 KeyStone 配置文件 +info "3. 检查 keystone.conf 配置文件" +if grep -q "^connection.*=.*mysql+pymysql://keystone:keystone@controller/keystone" /etc/keystone/keystone.conf 2>/dev/null; then + pass "数据库连接 [database] 配置正确" +else + fail "数据库连接 [database] 配置不匹配" +fi + +if grep -q "^provider.*=.*fernet" /etc/keystone/keystone.conf 2>/dev/null; then + pass "令牌 [token] 认证类型已按要求设置为 fernet" +else + fail "令牌 [token] 的 provider 未设置为 fernet" +fi + +# 4. 检查 Fernet 密钥 +info "4. 检查 Fernet 密钥库初始化" +if [ "$(ls -A /etc/keystone/fernet-keys 2>/dev/null)" ] && [ "$(ls -A /etc/keystone/credential-keys 2>/dev/null)" ]; then + pass "Fernet 与 Credential 密钥库已初始化" +else + fail "Fernet 密钥目录为空,未执行 fernet_setup" +fi + +# 5. 检查 Apache HTTP 状态 +info "5. 检查 Apache (HTTPD) 服务状态" +if grep -iq "^ServerName controller" /etc/httpd/conf/httpd.conf 2>/dev/null; then + pass "Apache ServerName 已配置为 controller" +else + fail "Apache ServerName 未配置正确" +fi + +if [ -L /etc/httpd/conf.d/wsgi-keystone.conf ]; then + pass "WSGI 虚拟主机链接已创建" +else + fail "wsgi-keystone.conf 的软链接不存在" +fi + +if systemctl is-active httpd &>/dev/null; then pass "httpd 服务正在运行"; else fail "httpd 服务未运行"; fi +if systemctl is-enabled httpd &>/dev/null; then pass "httpd 服务已允许开机自启"; else fail "httpd 未设置开机自启"; fi + +# 导入临时管理员环境变量用于后续 API 测试 +export OS_USERNAME=admin +export OS_PASSWORD=admin +export OS_PROJECT_NAME=admin +export OS_USER_DOMAIN_NAME=Default +export OS_PROJECT_DOMAIN_NAME=Default +export OS_AUTH_URL=http://controller:5000/v3 +export OS_IDENTITY_API_VERSION=3 +export NO_PROXY=localhost,127.0.0.1,controller + +# 6. Admin 身份令牌签发测试 (Bootstrap 验证) +info "6. 检查 KeyStone Bootstrap 及管理员权限" +if openstack token issue >/dev/null 2>&1; then + pass "管理员 (admin) 成功签发 Token (Bootstrap 初始化成功)" +else + fail "管理员 (admin) 无法签发 Token,Bootstrap 可能失败或 Apache 未响应" + echo -e "${RED}后续针对域、项目、用户的检查由于无法认证,将跳过!${NC}" + FAIL_CNT=$((FAIL_CNT+5)) + goto_end=1 +fi + + + +echo "" +echo -e "${YELLOW}====================================================${NC}" +echo -e "检查完毕!通过项: ${GREEN}${PASS_CNT}${NC},失败项: ${RED}${FAIL_CNT}${NC}" +if [ ${FAIL_CNT} -eq 0 ]; then + echo -e "${GREEN}太棒了!KeyStone 服务已完美搭建且工作正常!${NC}" +else + echo -e "${RED}你有 ${FAIL_CNT} 处配置未达标,请对照 [✘ 失败] 的提示,参考手册检查数据库或相关配置文件。${NC}" +fi +echo -e "${YELLOW}====================================================${NC}" diff --git a/3.3.sh b/3.3.sh new file mode 100755 index 0000000..2dc40a8 --- /dev/null +++ b/3.3.sh @@ -0,0 +1,139 @@ + +#!/bin/bash +# OpenStack 控制节点(Rocky) Glance 镜像服务部署检查脚本 +# 适用于: 3.3 Glance镜像服务部署 +# 强制设置字符集为 UTF-8,防止 OpenStack 客户端因为读取到中文/全角标点而崩溃报错 ascii codec +export LANG=en_US.UTF-8 +export LC_ALL=en_US.UTF-8 +# 颜色定义 +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 + +pass() { echo -e "${GREEN}[✔ 通过] $1${NC}"; ((PASS_CNT++)); } +fail() { echo -e "${RED}[✘ 失败] $1${NC}"; ((FAIL_CNT++)); } +info() { echo -e "${CYAN}[➜] $1${NC}"; } + +echo -e "${YELLOW}====================================================${NC}" +echo -e "${YELLOW} OpenStack Glance 镜像服务部署自测脚本 ${NC}" +echo -e "${YELLOW}====================================================${NC}" + +if [ "$EUID" -ne 0 ]; then + echo -e "${RED}请使用 root 权限执行此脚本!${NC}" + exit 1 +fi + +# 1. 检查数据库 +info "1. 检查 MariaDB 数据库创建与授权" +if mysql -uglance -pglance -e "SHOW DATABASES;" 2>/dev/null | grep -qw "glance"; then + pass "glance 数据库已创建且用户授权正确" + + if [ $(mysql -uglance -pglance -e "USE glance; SHOW TABLES;" 2>/dev/null | wc -l) -gt 2 ]; then + pass "glance 数据库表结构同步 (db_sync) 已完成" + else + fail "glance 数据库表为空,未执行同步命令" + fi +else + fail "glance 数据库未创建,或 glance 用户授权失败/密码不正确" +fi + +# 2. 检查软件包 +info "2. 检查依赖软件包安装" +if rpm -q openstack-glance &>/dev/null; then + pass "相关软件包 (openstack-glance) 已安装" +else + fail "软件包 openstack-glance 未安装" +fi + +# 导入临时管理员环境变量用于 API 测试 +export OS_USERNAME=admin +export OS_PASSWORD=admin +export OS_PROJECT_NAME=admin +export OS_USER_DOMAIN_NAME=Default +export OS_PROJECT_DOMAIN_NAME=Default +export OS_AUTH_URL=http://controller:5000/v3 +export OS_IDENTITY_API_VERSION=3 +export NO_PROXY=localhost,127.0.0.1,controller + +# 验证 Keystone 连通性 +if ! openstack token issue >/dev/null 2>&1; then + fail "当前无法与 Keystone 认证服务通信,请检查上节内容是否运行正常!" + echo -e "${RED}后续关于 Keystone 的依赖测试将被跳过!${NC}" + FAIL_CNT=$((FAIL_CNT+7)) + goto_end=1 +fi + +if [ -z "$goto_end" ]; then + # 3. 检查 Glance 凭证注册 + info "3. 检查 KeyStone 中的 Glance 账号与权限注册" + if openstack user show glance >/dev/null 2>&1; then pass "用户: glance 已在 KeyStone 中创建"; else fail "用户: glance 未在 KeyStone 中找到"; fi + + if openstack role assignment list --user glance --project service --role admin -f value --names 2>/dev/null | grep -q "glance"; then + pass "用户 glance 已成功加入 service 项目并绑定 admin 角色" + else + fail "未将 glance 用户绑定到 service 项目的 admin 角色" + fi + + if openstack service show image >/dev/null 2>&1; then pass "服务实体: image(glance) 已创建"; else fail "服务实体: image(glance) 未创建"; fi + + # 4. 检查端点 Endpoint 注册 + info "4. 检查 Endpoint API 端点注册" + endpoints=$(openstack endpoint list --service image -f value -c Interface) + if echo "$endpoints" | grep -qw "public" && echo "$endpoints" | grep -qw "internal" && echo "$endpoints" | grep -qw "admin"; then + pass "镜像服务的 public, internal, admin 端点已全部注册成功" + else + fail "镜像服务的端点注册不全,请检查 openstack endpoint list" + fi + + # 5. 检查 Glance 配置文件 + info "5. 检查 Glance 服务配置文件" + if grep -q "^connection.*=.*glance:glance@controller/glance" /etc/glance/glance-api.conf 2>/dev/null; then + pass "glance-api.conf 数据库配置正确" + else + fail "glance-api.conf 数据库连接 [database] 未配置或错误" + fi + + if grep -q "^auth_url.*=.*http://controller:5000" /etc/glance/glance-api.conf 2>/dev/null; then + pass "glance-api.conf 身份认证 [keystone_authtoken] 配置正确" + else + fail "glance-api.conf 身份认证 [keystone_authtoken] 配置有误" + fi + + if grep -q "^filesystem_store_datadir.*=.*?/var/lib/glance/images/" /etc/glance/glance-api.conf 2>/dev/null || grep -q "^stores.*=.*file" /etc/glance/glance-api.conf; then + pass "glance-api.conf 本地存储目录 [glance_store] 配置完成" + else + fail "glance-api.conf 存储方式 [glance_store] 配置有误" + fi + + # 6. 检查系统服务进程 + info "6. 检查 Glance 服务启动状态" + if systemctl is-active openstack-glance-api &>/dev/null; then pass "openstack-glance-api 服务正在运行"; else fail "openstack-glance-api 服务未运行 (手动补全系统命令了吗?)"; fi + if systemctl is-enabled openstack-glance-api &>/dev/null; then pass "openstack-glance-api 服务已设为开机自启"; else fail "openstack-glance-api 服务未设置开启自启"; fi + + if systemctl is-active openstack-glance-registry &>/dev/null; then pass "openstack-glance-registry 服务正在运行"; else fail "openstack-glance-registry 服务未运行"; fi + if systemctl is-enabled openstack-glance-registry &>/dev/null; then pass "openstack-glance-registry 服务已设为开机自启"; else fail "openstack-glance-registry 未设置开机自启"; fi + + # 7. 检查镜像上传测试结果 + info "7. 检查 CirrOS 镜像上传测试结果" + img_status=$(openstack image show cirros -f value -c status 2>/dev/null) + if [ "$img_status" == "active" ]; then + pass "测试镜像 cirros 已成功上传,且目前处于 active 状态!" + else + fail "未找到处于 active 状态的测试镜像 cirros,请检查上传步骤" + fi +fi + +echo "" +echo -e "${YELLOW}====================================================${NC}" +echo -e "检查完毕!通过项: ${GREEN}${PASS_CNT}${NC},失败项: ${RED}${FAIL_CNT}${NC}" +if [ ${FAIL_CNT} -eq 0 ]; then + echo -e "${GREEN}完美通过!Glance 镜像服务部署成功,已具备为虚拟机分发操作系统镜像的能力。${NC}" +else + echo -e "${RED}你有 ${FAIL_CNT} 处配置未达标,请对比报错提示与实验手册,仔细排查修改后再运行!${NC}" +fi +echo -e "${YELLOW}====================================================${NC}" diff --git a/3.4.sh b/3.4.sh new file mode 100755 index 0000000..1b3aeaf --- /dev/null +++ b/3.4.sh @@ -0,0 +1,413 @@ +#!/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 ~ /^$/ { 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 == "" { 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 diff --git a/env2-2.sh b/env2-2.sh new file mode 100755 index 0000000..3d0c7a4 --- /dev/null +++ b/env2-2.sh @@ -0,0 +1,116 @@ +#!/bin/bash + +set -euo pipefail + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_err() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# 检查 root 权限 +if [ "$EUID" -ne 0 ]; then + log_err "请使用 root 权限执行此脚本(如: sudo bash $0)" + exit 1 +fi + +log_info "==========================================" +log_info "开始初始化实验环境..." +log_info "==========================================" + +REPO_DIR="/etc/yum.repos.d" +BACKUP_DIR="/etc/yum.repos.d.bak" + +# 首次运行时备份原有软件源 +if [ ! -d "$BACKUP_DIR" ]; then + log_info "备份原有 yum 软件源..." + cp -a "$REPO_DIR" "$BACKUP_DIR" +fi + +# 配置 CentOS 7.9 基础源 +log_info "配置 CentOS 7.9 阿里云软件源..." + +cat > "$REPO_DIR/CentOS-Base.repo" <<'EOF' +[base] +name=CentOS-7 - Base - Aliyun +baseurl=http://mirrors.aliyun.com/centos-vault/7.9.2009/os/$basearch/ +gpgcheck=0 +enabled=1 + +[updates] +name=CentOS-7 - Updates - Aliyun +baseurl=http://mirrors.aliyun.com/centos-vault/7.9.2009/updates/$basearch/ +gpgcheck=0 +enabled=1 + +[extras] +name=CentOS-7 - Extras - Aliyun +baseurl=http://mirrors.aliyun.com/centos-vault/7.9.2009/extras/$basearch/ +gpgcheck=0 +enabled=1 +EOF + +# 配置 QEMU EV 源 +cat > "$REPO_DIR/CentOS-QEMU-EV.repo" <<'EOF' +[centos-qemu-ev] +name=CentOS-7 - QEMU EV - Aliyun +baseurl=http://mirrors.aliyun.com/centos-vault/7.9.2009/virt/$basearch/kvm-common/ +gpgcheck=0 +enabled=1 +EOF + +# 配置 OpenStack Rocky 源 +cat > "$REPO_DIR/CentOS-OpenStack-rocky.repo" <<'EOF' +[centos-openstack-rocky] +name=CentOS-7 - OpenStack Rocky - Aliyun +baseurl=http://mirrors.aliyun.com/centos-vault/7.9.2009/cloud/$basearch/openstack-rocky/ +gpgcheck=0 +enabled=1 +exclude=sip,PyQt4 +EOF + +# 删除不再需要的 Ceph 源,避免 yum 读取其元数据 +rm -f "$REPO_DIR/CentOS-Ceph-Luminous.repo" + + +# 清除旧仓库元数据,保留已下载的软件包缓存 +log_info "刷新软件源元数据..." +yum clean metadata > /dev/null 2>&1 + +# 安装 KVM / libvirt +log_info "安装/检查虚拟化软件包..." + +yum install -y \ + qemu-kvm \ + libvirt \ + virt-install \ + bridge-utils \ + qemu-img + +# 启动 libvirtd +log_info "启动 libvirtd 服务..." + +systemctl enable --now libvirtd > /dev/null + +# 验证服务状态 +if systemctl is-active --quiet libvirtd; then + log_info "libvirtd 状态正常。" +else + log_err "libvirtd 启动失败!" + exit 1 +fi + +log_info "==========================================" +log_info "实验环境初始化完成!" +log_info "==========================================" \ No newline at end of file diff --git a/env3-1-local.sh b/env3-1-local.sh new file mode 100755 index 0000000..81dce5d --- /dev/null +++ b/env3-1-local.sh @@ -0,0 +1,307 @@ +#!/bin/bash + +set -euo pipefail + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_err() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# 检查 root 权限 +if [ "$EUID" -ne 0 ]; then + log_err "请使用 root 权限执行此脚本(如: sudo bash $0)" + exit 1 +fi + +log_info "==========================================" +log_info "开始初始化实验环境..." +log_info "==========================================" + +# 直接连接终端,保留 yum 原生下载进度条;不要通过 tee 管道转存输出。 +run_yum() { + local status + log_info "执行 yum $*(下载速度取决于网络和镜像源)..." + # 仅启用本地仓库,避免系统中其他已启用的公网仓库参与下载。 + if yum --disablerepo='*' \ + --enablerepo=local-base,local-updates,local-extras,local-qemu-ev,local-openstack-rocky \ + "$@"; then + log_info "yum 操作完成。" + else + status=$? + log_err "yum 操作失败(退出码 $status),请查看上方错误信息。" + return "$status" + fi +} + +REPO_DIR="/etc/yum.repos.d" +BACKUP_DIR="/etc/yum.repos.d.bak" + +# 首次运行时备份原有软件源 +if [ ! -d "$BACKUP_DIR" ]; then + log_info "备份原有 yum 软件源..." + cp -a "$REPO_DIR" "$BACKUP_DIR" +fi + +# 配置 CentOS 7.9 基础源 +log_info "配置 CentOS 7.9 本地软件源(192.168.192.205:3080)..." + +cat > "$REPO_DIR/CentOS-Base.repo" <<'EOF' +[local-base] +name=Local - Base +baseurl=http://192.168.192.205:3080/vault-base/ +gpgcheck=0 +enabled=1 + +[local-updates] +name=Local - Updates +baseurl=http://192.168.192.205:3080/vault-updates/ +gpgcheck=0 +enabled=1 + +[local-extras] +name=Local - Extras +baseurl=http://192.168.192.205:3080/vault-extras/ +gpgcheck=0 +enabled=1 +EOF + +# 配置 QEMU EV 源 +cat > "$REPO_DIR/CentOS-QEMU-EV.repo" <<'EOF' +[local-qemu-ev] +name=Local - QEMU EV +baseurl=http://192.168.192.205:3080/vault-centos-qemu-ev/ +gpgcheck=0 +enabled=1 +EOF + +# 配置 OpenStack Rocky 源 +cat > "$REPO_DIR/CentOS-OpenStack-rocky.repo" <<'EOF' +[local-openstack-rocky] +name=Local - OpenStack Rocky +baseurl=http://192.168.192.205:3080/vault-centos-openstack-rocky/ +gpgcheck=0 +enabled=1 +exclude=sip,PyQt4 +EOF + +# 删除不再需要的 Ceph 源,避免 yum 读取其元数据 +rm -f "$REPO_DIR/CentOS-Ceph-Luminous.repo" + + +# 清除旧仓库元数据,保留已下载的软件包缓存 +log_info "刷新软件源元数据..." +run_yum clean metadata + +# 安装 KVM / libvirt +log_info "安装/检查虚拟化软件包..." + +run_yum install -y \ + qemu-kvm \ + libvirt \ + virt-install \ + bridge-utils \ + qemu-img + +# 启动 libvirtd +log_info "启动 libvirtd 服务..." + +systemctl enable --now libvirtd > /dev/null + +# 验证服务状态 +if systemctl is-active --quiet libvirtd; then + log_info "libvirtd 状态正常。" +else + log_err "libvirtd 启动失败!" + exit 1 +fi + +log_info "==========================================" +log_info "基础虚拟化环境初始化完成,开始配置控制节点..." +log_info "==========================================" + +CONTROLLER_IP="192.168.30.129" +CONTROLLER_HOSTNAME="controller" + +# 首次修改时备份配置,重复运行不覆盖原始备份。 +backup_config() { + if [ -f "$1" ] && [ ! -e "$1.env3-1.bak" ]; then + cp -a "$1" "$1.env3-1.bak" + fi +} + +log_info "关闭防火墙和 SELinux..." +if systemctl list-unit-files firewalld.service --no-legend | grep -q '^firewalld.service'; then + systemctl stop firewalld + systemctl disable firewalld +fi +if command -v getenforce > /dev/null 2>&1; then + if [ "$(getenforce)" = "Enforcing" ]; then + setenforce 0 + fi +fi +if [ -f /etc/selinux/config ]; then + backup_config /etc/selinux/config + sed -i '/^[[:space:]]*SELINUX=/d' /etc/selinux/config + echo 'SELINUX=disabled' >> /etc/selinux/config +fi + +log_info "配置主机名和 hosts 解析..." +hostnamectl set-hostname "$CONTROLLER_HOSTNAME" +backup_config /etc/hosts +# 移除旧 controller 映射,保留同一行的其他别名及注释。 +HOSTS_TMP=$(mktemp) +awk -v host="$CONTROLLER_HOSTNAME" ' + /^[[:space:]]*#/ || NF == 0 { print; next } + { + comment = "" + pos = index($0, "#") + if (pos) { + comment = substr($0, pos) + $0 = substr($0, 1, pos - 1) + } + line = $1 + aliases = 0 + for (i = 2; i <= NF; i++) { + if ($i != host) { + line = line " " $i + aliases++ + } + } + if (aliases) print line (comment == "" ? "" : " " comment) + else if (comment != "") print comment + } +' /etc/hosts > "$HOSTS_TMP" +cat "$HOSTS_TMP" > /etc/hosts +rm -f "$HOSTS_TMP" +printf '%s %s\n' "$CONTROLLER_IP" "$CONTROLLER_HOSTNAME" >> /etc/hosts + +log_info "升级系统软件包并安装 OpenStack 基础服务..." +run_yum -y upgrade +run_yum install -y chrony python2-openstackclient \ + mariadb mariadb-server python2-PyMySQL \ + rabbitmq-server memcached python-memcached etcd + +log_info "配置 chrony 时间服务器..." +backup_config /etc/chrony.conf +if ! grep -Eq '^[[:space:]]*allow[[:space:]]+all([[:space:]]*(#.*)?)?$' /etc/chrony.conf; then + printf '\n# 允许所有网段的机器访问同步时间\nallow all\n' >> /etc/chrony.conf +fi +systemctl enable chronyd +systemctl restart chronyd + +log_info "配置 MariaDB..." +mkdir -p /etc/my.cnf.d +backup_config /etc/my.cnf.d/openstack.cnf +cat > /etc/my.cnf.d/openstack.cnf <<'EOF' +[mysqld] +bind-address = 0.0.0.0 +# 默认使用 InnoDB,每个表的数据独立保存成文件。 +default-storage-engine = innodb +innodb_file_per_table = on +# 调高最大连接数,并使用 UTF-8 字符集。 +max_connections = 4096 +collation-server = utf8_general_ci +character-set-server = utf8 +EOF +systemctl enable mariadb +systemctl restart mariadb + +log_info "初始化 MariaDB 安全配置及 root 远程登录权限..." +# CentOS 7 的 MariaDB 5.5:自动执行 mysql_secure_installation 的等效操作。 +# 使用权限为 600 的临时客户端配置,避免把密码放进进程参数。 +MYSQL_AUTH_FILE=$(mktemp) +trap 'rm -f "$MYSQL_AUTH_FILE"' EXIT +chmod 600 "$MYSQL_AUTH_FILE" +cat > "$MYSQL_AUTH_FILE" <<'EOF' +[client] +user=root +password=123456 +protocol=socket +EOF +MYSQL_CMD=(mysql --defaults-extra-file="$MYSQL_AUTH_FILE") +if ! "${MYSQL_CMD[@]}" -e 'SELECT 1' > /dev/null 2>&1; then + MYSQL_CMD=(mysql --no-defaults --user=root --protocol=socket) + if ! "${MYSQL_CMD[@]}" -e 'SELECT 1' > /dev/null 2>&1; then + log_err "无法使用空密码或预设密码连接 MariaDB root,请检查已有数据库凭据。" + exit 1 + fi +fi +"${MYSQL_CMD[@]}" <<'SQL' +DELETE FROM mysql.user WHERE User = ''; +DELETE FROM mysql.user + WHERE User = 'root' AND Host NOT IN ('localhost', '127.0.0.1', '::1'); +UPDATE mysql.user SET Password = PASSWORD('123456') WHERE User = 'root'; +DROP DATABASE IF EXISTS test; +DELETE FROM mysql.db WHERE Db = 'test' OR Db = 'test\_%'; +FLUSH PRIVILEGES; +GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '123456' WITH GRANT OPTION; +FLUSH PRIVILEGES; +SQL +mysql --defaults-extra-file="$MYSQL_AUTH_FILE" -e 'SELECT 1' > /dev/null +rm -f "$MYSQL_AUTH_FILE" +trap - EXIT + +log_info "配置 RabbitMQ 用户及权限..." +systemctl enable rabbitmq-server +systemctl start rabbitmq-server +RABBITMQ_USERS=$(rabbitmqctl list_users -q) +if awk '$1 == "openstack" { found = 1 } END { exit !found }' <<< "$RABBITMQ_USERS"; then + rabbitmqctl change_password openstack openstack +else + rabbitmqctl add_user openstack openstack +fi +rabbitmqctl set_permissions -p / openstack '.*' '.*' '.*' + +log_info "配置 Memcached..." +backup_config /etc/sysconfig/memcached +sed -i '/^[[:space:]]*OPTIONS=/d' /etc/sysconfig/memcached +echo 'OPTIONS="-l 127.0.0.1,::1,controller"' >> /etc/sysconfig/memcached +systemctl enable memcached +systemctl restart memcached + +log_info "配置 Etcd..." +backup_config /etc/etcd/etcd.conf +cat > /etc/etcd/etcd.conf <<'EOF' +#[Member] +ETCD_LISTEN_PEER_URLS="http://0.0.0.0:2380" +ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0:2379" +ETCD_NAME="controller" + +#[Clustering] +ETCD_INITIAL_ADVERTISE_PEER_URLS="http://controller:2380" +ETCD_ADVERTISE_CLIENT_URLS="http://controller:2379" +ETCD_INITIAL_CLUSTER="controller=http://controller:2380" +ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster-01" +ETCD_INITIAL_CLUSTER_STATE="new" +EOF +systemctl enable etcd +systemctl restart etcd + +log_info "检查各项服务状态..." +for service in libvirtd chronyd mariadb rabbitmq-server memcached etcd; do + if systemctl is-active --quiet "$service"; then + log_info "$service 状态正常。" + else + log_err "$service 未正常运行,请使用 journalctl -u $service 查看日志。" + exit 1 + fi +done + +log_info "==========================================" +log_info "OpenStack 控制节点初始化完成!" +log_info "控制节点:$CONTROLLER_HOSTNAME ($CONTROLLER_IP)" +log_info "MariaDB root 密码:123456,已允许远程登录。" +log_info "RabbitMQ 用户/密码:openstack / openstack" +log_info "==========================================" diff --git a/env3-1.sh b/env3-1.sh new file mode 100755 index 0000000..cc63a6a --- /dev/null +++ b/env3-1.sh @@ -0,0 +1,304 @@ +#!/bin/bash + +set -euo pipefail + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_err() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# 检查 root 权限 +if [ "$EUID" -ne 0 ]; then + log_err "请使用 root 权限执行此脚本(如: sudo bash $0)" + exit 1 +fi + +log_info "==========================================" +log_info "开始初始化实验环境..." +log_info "==========================================" + +# 直接连接终端,保留 yum 原生下载进度条;不要通过 tee 管道转存输出。 +run_yum() { + local status + log_info "执行 yum $*(下载速度取决于网络和镜像源)..." + if yum "$@"; then + log_info "yum 操作完成。" + else + status=$? + log_err "yum 操作失败(退出码 $status),请查看上方错误信息。" + return "$status" + fi +} + +REPO_DIR="/etc/yum.repos.d" +BACKUP_DIR="/etc/yum.repos.d.bak" + +# 首次运行时备份原有软件源 +if [ ! -d "$BACKUP_DIR" ]; then + log_info "备份原有 yum 软件源..." + cp -a "$REPO_DIR" "$BACKUP_DIR" +fi + +# 配置 CentOS 7.9 基础源 +log_info "配置 CentOS 7.9 腾讯云软件源..." + +cat > "$REPO_DIR/CentOS-Base.repo" <<'EOF' +[base] +name=CentOS-7 - Base - Tencent +baseurl=https://mirrors.cloud.tencent.com/centos-vault/7.9.2009/os/$basearch/ +gpgcheck=0 +enabled=1 + +[updates] +name=CentOS-7 - Updates - Tencent +baseurl=https://mirrors.cloud.tencent.com/centos-vault/7.9.2009/updates/$basearch/ +gpgcheck=0 +enabled=1 + +[extras] +name=CentOS-7 - Extras - Tencent +baseurl=https://mirrors.cloud.tencent.com/centos-vault/7.9.2009/extras/$basearch/ +gpgcheck=0 +enabled=1 +EOF + +# 配置 QEMU EV 源 +cat > "$REPO_DIR/CentOS-QEMU-EV.repo" <<'EOF' +[centos-qemu-ev] +name=CentOS-7 - QEMU EV - Tencent +baseurl=https://mirrors.cloud.tencent.com/centos-vault/7.9.2009/virt/$basearch/kvm-common/ +gpgcheck=0 +enabled=1 +EOF + +# 配置 OpenStack Rocky 源 +cat > "$REPO_DIR/CentOS-OpenStack-rocky.repo" <<'EOF' +[centos-openstack-rocky] +name=CentOS-7 - OpenStack Rocky - Tencent +baseurl=https://mirrors.cloud.tencent.com/centos-vault/7.9.2009/cloud/$basearch/openstack-rocky/ +gpgcheck=0 +enabled=1 +exclude=sip,PyQt4 +EOF + +# 删除不再需要的 Ceph 源,避免 yum 读取其元数据 +rm -f "$REPO_DIR/CentOS-Ceph-Luminous.repo" + + +# 清除旧仓库元数据,保留已下载的软件包缓存 +log_info "刷新软件源元数据..." +run_yum clean metadata + +# 安装 KVM / libvirt +log_info "安装/检查虚拟化软件包..." + +run_yum install -y \ + qemu-kvm \ + libvirt \ + virt-install \ + bridge-utils \ + qemu-img + +# 启动 libvirtd +log_info "启动 libvirtd 服务..." + +systemctl enable --now libvirtd > /dev/null + +# 验证服务状态 +if systemctl is-active --quiet libvirtd; then + log_info "libvirtd 状态正常。" +else + log_err "libvirtd 启动失败!" + exit 1 +fi + +log_info "==========================================" +log_info "基础虚拟化环境初始化完成,开始配置控制节点..." +log_info "==========================================" + +CONTROLLER_IP="192.168.30.129" +CONTROLLER_HOSTNAME="controller" + +# 首次修改时备份配置,重复运行不覆盖原始备份。 +backup_config() { + if [ -f "$1" ] && [ ! -e "$1.env3-1.bak" ]; then + cp -a "$1" "$1.env3-1.bak" + fi +} + +log_info "关闭防火墙和 SELinux..." +if systemctl list-unit-files firewalld.service --no-legend | grep -q '^firewalld.service'; then + systemctl stop firewalld + systemctl disable firewalld +fi +if command -v getenforce > /dev/null 2>&1; then + if [ "$(getenforce)" = "Enforcing" ]; then + setenforce 0 + fi +fi +if [ -f /etc/selinux/config ]; then + backup_config /etc/selinux/config + sed -i '/^[[:space:]]*SELINUX=/d' /etc/selinux/config + echo 'SELINUX=disabled' >> /etc/selinux/config +fi + +log_info "配置主机名和 hosts 解析..." +hostnamectl set-hostname "$CONTROLLER_HOSTNAME" +backup_config /etc/hosts +# 移除旧 controller 映射,保留同一行的其他别名及注释。 +HOSTS_TMP=$(mktemp) +awk -v host="$CONTROLLER_HOSTNAME" ' + /^[[:space:]]*#/ || NF == 0 { print; next } + { + comment = "" + pos = index($0, "#") + if (pos) { + comment = substr($0, pos) + $0 = substr($0, 1, pos - 1) + } + line = $1 + aliases = 0 + for (i = 2; i <= NF; i++) { + if ($i != host) { + line = line " " $i + aliases++ + } + } + if (aliases) print line (comment == "" ? "" : " " comment) + else if (comment != "") print comment + } +' /etc/hosts > "$HOSTS_TMP" +cat "$HOSTS_TMP" > /etc/hosts +rm -f "$HOSTS_TMP" +printf '%s %s\n' "$CONTROLLER_IP" "$CONTROLLER_HOSTNAME" >> /etc/hosts + +log_info "升级系统软件包并安装 OpenStack 基础服务..." +run_yum -y upgrade +run_yum install -y chrony python2-openstackclient \ + mariadb mariadb-server python2-PyMySQL \ + rabbitmq-server memcached python-memcached etcd + +log_info "配置 chrony 时间服务器..." +backup_config /etc/chrony.conf +if ! grep -Eq '^[[:space:]]*allow[[:space:]]+all([[:space:]]*(#.*)?)?$' /etc/chrony.conf; then + printf '\n# 允许所有网段的机器访问同步时间\nallow all\n' >> /etc/chrony.conf +fi +systemctl enable chronyd +systemctl restart chronyd + +log_info "配置 MariaDB..." +mkdir -p /etc/my.cnf.d +backup_config /etc/my.cnf.d/openstack.cnf +cat > /etc/my.cnf.d/openstack.cnf <<'EOF' +[mysqld] +bind-address = 0.0.0.0 +# 默认使用 InnoDB,每个表的数据独立保存成文件。 +default-storage-engine = innodb +innodb_file_per_table = on +# 调高最大连接数,并使用 UTF-8 字符集。 +max_connections = 4096 +collation-server = utf8_general_ci +character-set-server = utf8 +EOF +systemctl enable mariadb +systemctl restart mariadb + +log_info "初始化 MariaDB 安全配置及 root 远程登录权限..." +# CentOS 7 的 MariaDB 5.5:自动执行 mysql_secure_installation 的等效操作。 +# 使用权限为 600 的临时客户端配置,避免把密码放进进程参数。 +MYSQL_AUTH_FILE=$(mktemp) +trap 'rm -f "$MYSQL_AUTH_FILE"' EXIT +chmod 600 "$MYSQL_AUTH_FILE" +cat > "$MYSQL_AUTH_FILE" <<'EOF' +[client] +user=root +password=123456 +protocol=socket +EOF +MYSQL_CMD=(mysql --defaults-extra-file="$MYSQL_AUTH_FILE") +if ! "${MYSQL_CMD[@]}" -e 'SELECT 1' > /dev/null 2>&1; then + MYSQL_CMD=(mysql --no-defaults --user=root --protocol=socket) + if ! "${MYSQL_CMD[@]}" -e 'SELECT 1' > /dev/null 2>&1; then + log_err "无法使用空密码或预设密码连接 MariaDB root,请检查已有数据库凭据。" + exit 1 + fi +fi +"${MYSQL_CMD[@]}" <<'SQL' +DELETE FROM mysql.user WHERE User = ''; +DELETE FROM mysql.user + WHERE User = 'root' AND Host NOT IN ('localhost', '127.0.0.1', '::1'); +UPDATE mysql.user SET Password = PASSWORD('123456') WHERE User = 'root'; +DROP DATABASE IF EXISTS test; +DELETE FROM mysql.db WHERE Db = 'test' OR Db = 'test\_%'; +FLUSH PRIVILEGES; +GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '123456' WITH GRANT OPTION; +FLUSH PRIVILEGES; +SQL +mysql --defaults-extra-file="$MYSQL_AUTH_FILE" -e 'SELECT 1' > /dev/null +rm -f "$MYSQL_AUTH_FILE" +trap - EXIT + +log_info "配置 RabbitMQ 用户及权限..." +systemctl enable rabbitmq-server +systemctl start rabbitmq-server +RABBITMQ_USERS=$(rabbitmqctl list_users -q) +if awk '$1 == "openstack" { found = 1 } END { exit !found }' <<< "$RABBITMQ_USERS"; then + rabbitmqctl change_password openstack openstack +else + rabbitmqctl add_user openstack openstack +fi +rabbitmqctl set_permissions -p / openstack '.*' '.*' '.*' + +log_info "配置 Memcached..." +backup_config /etc/sysconfig/memcached +sed -i '/^[[:space:]]*OPTIONS=/d' /etc/sysconfig/memcached +echo 'OPTIONS="-l 127.0.0.1,::1,controller"' >> /etc/sysconfig/memcached +systemctl enable memcached +systemctl restart memcached + +log_info "配置 Etcd..." +backup_config /etc/etcd/etcd.conf +cat > /etc/etcd/etcd.conf <<'EOF' +#[Member] +ETCD_LISTEN_PEER_URLS="http://0.0.0.0:2380" +ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0:2379" +ETCD_NAME="controller" + +#[Clustering] +ETCD_INITIAL_ADVERTISE_PEER_URLS="http://controller:2380" +ETCD_ADVERTISE_CLIENT_URLS="http://controller:2379" +ETCD_INITIAL_CLUSTER="controller=http://controller:2380" +ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster-01" +ETCD_INITIAL_CLUSTER_STATE="new" +EOF +systemctl enable etcd +systemctl restart etcd + +log_info "检查各项服务状态..." +for service in libvirtd chronyd mariadb rabbitmq-server memcached etcd; do + if systemctl is-active --quiet "$service"; then + log_info "$service 状态正常。" + else + log_err "$service 未正常运行,请使用 journalctl -u $service 查看日志。" + exit 1 + fi +done + +log_info "==========================================" +log_info "OpenStack 控制节点初始化完成!" +log_info "控制节点:$CONTROLLER_HOSTNAME ($CONTROLLER_IP)" +log_info "MariaDB root 密码:123456,已允许远程登录。" +log_info "RabbitMQ 用户/密码:openstack / openstack" +log_info "==========================================" diff --git a/env3-2-local.sh b/env3-2-local.sh new file mode 100755 index 0000000..fcb42be --- /dev/null +++ b/env3-2-local.sh @@ -0,0 +1,353 @@ +#!/bin/bash + +set -euo pipefail + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_err() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# 检查 root 权限 +if [ "$EUID" -ne 0 ]; then + log_err "请使用 root 权限执行此脚本(如: sudo bash $0)" + exit 1 +fi + +log_info "==========================================" +log_info "开始初始化实验环境..." +log_info "==========================================" + +# 直接连接终端,保留 yum 原生下载进度条;不要通过 tee 管道转存输出。 +run_yum() { + local status + log_info "执行 yum $*(下载速度取决于网络和镜像源)..." + # 仅启用本地仓库,避免系统中其他已启用的公网仓库参与下载。 + if yum --disablerepo='*' \ + --enablerepo=local-base,local-updates,local-extras,local-qemu-ev,local-openstack-rocky \ + "$@"; then + log_info "yum 操作完成。" + else + status=$? + log_err "yum 操作失败(退出码 $status),请查看上方错误信息。" + return "$status" + fi +} + +REPO_DIR="/etc/yum.repos.d" +BACKUP_DIR="/etc/yum.repos.d.bak" + +# 首次运行时备份原有软件源 +if [ ! -d "$BACKUP_DIR" ]; then + log_info "备份原有 yum 软件源..." + cp -a "$REPO_DIR" "$BACKUP_DIR" +fi + +# 配置 CentOS 7.9 基础源 +log_info "配置 CentOS 7.9 本地软件源(192.168.192.205:3080)..." + +cat > "$REPO_DIR/CentOS-Base.repo" <<'EOF' +[local-base] +name=Local - Base +baseurl=http://192.168.192.205:3080/vault-base/ +gpgcheck=0 +enabled=1 + +[local-updates] +name=Local - Updates +baseurl=http://192.168.192.205:3080/vault-updates/ +gpgcheck=0 +enabled=1 + +[local-extras] +name=Local - Extras +baseurl=http://192.168.192.205:3080/vault-extras/ +gpgcheck=0 +enabled=1 +EOF + +# 配置 QEMU EV 源 +cat > "$REPO_DIR/CentOS-QEMU-EV.repo" <<'EOF' +[local-qemu-ev] +name=Local - QEMU EV +baseurl=http://192.168.192.205:3080/vault-centos-qemu-ev/ +gpgcheck=0 +enabled=1 +EOF + +# 配置 OpenStack Rocky 源 +cat > "$REPO_DIR/CentOS-OpenStack-rocky.repo" <<'EOF' +[local-openstack-rocky] +name=Local - OpenStack Rocky +baseurl=http://192.168.192.205:3080/vault-centos-openstack-rocky/ +gpgcheck=0 +enabled=1 +exclude=sip,PyQt4 +EOF + +# 删除不再需要的 Ceph 源,避免 yum 读取其元数据 +rm -f "$REPO_DIR/CentOS-Ceph-Luminous.repo" + + +# 清除旧仓库元数据,保留已下载的软件包缓存 +log_info "刷新软件源元数据..." +run_yum clean metadata + +CONTROLLER_IP="192.168.30.129" +CONTROLLER_HOSTNAME="controller" + +# 首次修改时备份配置,重复运行不覆盖原始备份。 +backup_config() { + if [ -f "$1" ] && [ ! -e "$1.env3-2.bak" ]; then + cp -a "$1" "$1.env3-2.bak" + fi +} + +log_info "关闭防火墙和 SELinux..." +if systemctl list-unit-files firewalld.service --no-legend | grep -q '^firewalld.service'; then + systemctl stop firewalld + systemctl disable firewalld +fi +if command -v getenforce > /dev/null 2>&1; then + if [ "$(getenforce)" = "Enforcing" ]; then + setenforce 0 + fi +fi +if [ -f /etc/selinux/config ]; then + backup_config /etc/selinux/config + sed -i '/^[[:space:]]*SELINUX=/d' /etc/selinux/config + echo 'SELINUX=disabled' >> /etc/selinux/config +fi + +log_info "配置主机名和 hosts 解析..." +hostnamectl set-hostname "$CONTROLLER_HOSTNAME" +backup_config /etc/hosts +# 移除旧 controller 映射,保留同一行的其他别名及注释。 +HOSTS_TMP=$(mktemp) +awk -v host="$CONTROLLER_HOSTNAME" ' + /^[[:space:]]*#/ || NF == 0 { print; next } + { + comment = "" + pos = index($0, "#") + if (pos) { + comment = substr($0, pos) + $0 = substr($0, 1, pos - 1) + } + line = $1 + aliases = 0 + for (i = 2; i <= NF; i++) { + if ($i != host) { + line = line " " $i + aliases++ + } + } + if (aliases) print line (comment == "" ? "" : " " comment) + else if (comment != "") print comment + } +' /etc/hosts > "$HOSTS_TMP" +cat "$HOSTS_TMP" > /etc/hosts +rm -f "$HOSTS_TMP" +printf '%s %s\n' "$CONTROLLER_IP" "$CONTROLLER_HOSTNAME" >> /etc/hosts + +log_info "升级系统软件包,合并安装虚拟化、OpenStack 基础服务和 Keystone..." +run_yum -y upgrade +run_yum install -y chrony python2-openstackclient \ + mariadb mariadb-server python2-PyMySQL \ + rabbitmq-server memcached python-memcached etcd \ + qemu-kvm libvirt virt-install bridge-utils qemu-img \ + openstack-keystone httpd mod_wsgi + +# 启动 libvirtd +log_info "启动 libvirtd 服务..." + +systemctl enable --now libvirtd > /dev/null + +# 验证服务状态 +if systemctl is-active --quiet libvirtd; then + log_info "libvirtd 状态正常。" +else + log_err "libvirtd 启动失败!" + exit 1 +fi + + +log_info "配置 chrony 时间服务器..." +backup_config /etc/chrony.conf +if ! grep -Eq '^[[:space:]]*allow[[:space:]]+all([[:space:]]*(#.*)?)?$' /etc/chrony.conf; then + printf '\n# 允许所有网段的机器访问同步时间\nallow all\n' >> /etc/chrony.conf +fi +systemctl enable chronyd +systemctl restart chronyd + +log_info "配置 MariaDB..." +mkdir -p /etc/my.cnf.d +backup_config /etc/my.cnf.d/openstack.cnf +cat > /etc/my.cnf.d/openstack.cnf <<'EOF' +[mysqld] +bind-address = 0.0.0.0 +# 默认使用 InnoDB,每个表的数据独立保存成文件。 +default-storage-engine = innodb +innodb_file_per_table = on +# 调高最大连接数,并使用 UTF-8 字符集。 +max_connections = 4096 +collation-server = utf8_general_ci +character-set-server = utf8 +EOF +systemctl enable mariadb +systemctl restart mariadb + +log_info "初始化 MariaDB 安全配置及 root 远程登录权限..." +# CentOS 7 的 MariaDB 5.5:自动执行 mysql_secure_installation 的等效操作。 +# 使用权限为 600 的临时客户端配置,避免把密码放进进程参数。 +MYSQL_AUTH_FILE=$(mktemp) +trap 'rm -f "$MYSQL_AUTH_FILE"' EXIT +chmod 600 "$MYSQL_AUTH_FILE" +cat > "$MYSQL_AUTH_FILE" <<'EOF' +[client] +user=root +password=123456 +protocol=socket +EOF +MYSQL_CMD=(mysql --defaults-extra-file="$MYSQL_AUTH_FILE") +if ! "${MYSQL_CMD[@]}" -e 'SELECT 1' > /dev/null 2>&1; then + MYSQL_CMD=(mysql --no-defaults --user=root --protocol=socket) + if ! "${MYSQL_CMD[@]}" -e 'SELECT 1' > /dev/null 2>&1; then + log_err "无法使用空密码或预设密码连接 MariaDB root,请检查已有数据库凭据。" + exit 1 + fi +fi +"${MYSQL_CMD[@]}" <<'SQL' +DELETE FROM mysql.user WHERE User = ''; +DELETE FROM mysql.user + WHERE User = 'root' AND Host NOT IN ('localhost', '127.0.0.1', '::1'); +UPDATE mysql.user SET Password = PASSWORD('123456') WHERE User = 'root'; +DROP DATABASE IF EXISTS test; +DELETE FROM mysql.db WHERE Db = 'test' OR Db = 'test\_%'; +FLUSH PRIVILEGES; +GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '123456' WITH GRANT OPTION; +FLUSH PRIVILEGES; +SQL +log_info "创建 Keystone 数据库并授权..." +mysql --defaults-extra-file="$MYSQL_AUTH_FILE" <<'SQL' +CREATE DATABASE IF NOT EXISTS keystone; +GRANT ALL PRIVILEGES ON keystone.* TO 'keystone'@'localhost' IDENTIFIED BY 'keystone'; +GRANT ALL PRIVILEGES ON keystone.* TO 'keystone'@'%' IDENTIFIED BY 'keystone'; +FLUSH PRIVILEGES; +SQL +rm -f "$MYSQL_AUTH_FILE" +trap - EXIT + +log_info "配置 RabbitMQ 用户及权限..." +systemctl enable rabbitmq-server +systemctl start rabbitmq-server +RABBITMQ_USERS=$(rabbitmqctl list_users -q) +if awk '$1 == "openstack" { found = 1 } END { exit !found }' <<< "$RABBITMQ_USERS"; then + rabbitmqctl change_password openstack openstack +else + rabbitmqctl add_user openstack openstack +fi +rabbitmqctl set_permissions -p / openstack '.*' '.*' '.*' + +log_info "配置 Memcached..." +backup_config /etc/sysconfig/memcached +sed -i '/^[[:space:]]*OPTIONS=/d' /etc/sysconfig/memcached +echo 'OPTIONS="-l 127.0.0.1,::1,controller"' >> /etc/sysconfig/memcached +systemctl enable memcached +systemctl restart memcached + +log_info "配置 Etcd..." +backup_config /etc/etcd/etcd.conf +cat > /etc/etcd/etcd.conf <<'EOF' +#[Member] +ETCD_LISTEN_PEER_URLS="http://0.0.0.0:2380" +ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0:2379" +ETCD_NAME="controller" + +#[Clustering] +ETCD_INITIAL_ADVERTISE_PEER_URLS="http://controller:2380" +ETCD_ADVERTISE_CLIENT_URLS="http://controller:2379" +ETCD_INITIAL_CLUSTER="controller=http://controller:2380" +ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster-01" +ETCD_INITIAL_CLUSTER_STATE="new" +EOF +systemctl enable etcd +systemctl restart etcd + +log_info "配置 Keystone 数据库连接及 Fernet 令牌..." +backup_config /etc/keystone/keystone.conf +cat > /etc/keystone/keystone.conf <<'EOF' +[database] +connection = mysql+pymysql://keystone:keystone@controller/keystone +[token] +provider = fernet +EOF +chown root:keystone /etc/keystone/keystone.conf +chmod 640 /etc/keystone/keystone.conf + +log_info "同步 Keystone 数据库表结构..." +su -s /bin/sh -c "keystone-manage db_sync" keystone + +# 使用 setup 命令初始化密钥库,不删除已有密钥。 +log_info "初始化 Fernet 和 credential 密钥库..." +keystone-manage fernet_setup --keystone-user keystone --keystone-group keystone +keystone-manage credential_setup --keystone-user keystone --keystone-group keystone + +log_info "初始化 Keystone 管理员及服务端点..." +keystone-manage bootstrap --bootstrap-password admin \ + --bootstrap-admin-url http://controller:5000/v3 \ + --bootstrap-public-url http://controller:5000/v3 \ + --bootstrap-internal-url http://controller:5000/v3 \ + --bootstrap-region-id RegionOne + +log_info "配置 Apache HTTP 服务..." +backup_config /etc/httpd/conf/httpd.conf +# 删除已有生效的 ServerName,避免重复追加。 +sed -i '/^[[:space:]]*ServerName[[:space:]]/d' /etc/httpd/conf/httpd.conf +printf '\nServerName controller\n' >> /etc/httpd/conf/httpd.conf + +WSGI_SOURCE="/usr/share/keystone/wsgi-keystone.conf" +WSGI_LINK="/etc/httpd/conf.d/wsgi-keystone.conf" +if [ ! -f "$WSGI_SOURCE" ]; then + log_err "缺少 Keystone WSGI 配置:$WSGI_SOURCE" + exit 1 +fi +# 备份已有普通配置文件;重复运行时更新软链接。 +if [ ! -L "$WSGI_LINK" ]; then + backup_config "$WSGI_LINK" +fi +ln -sfnT "$WSGI_SOURCE" "$WSGI_LINK" + +log_info "检查 Apache 配置并启动服务..." +httpd -t +systemctl enable httpd +systemctl restart httpd + +log_info "检查各项服务状态..." +for service in libvirtd chronyd mariadb rabbitmq-server memcached etcd httpd; do + if systemctl is-active --quiet "$service"; then + log_info "$service 状态正常。" + else + log_err "$service 未正常运行,请使用 journalctl -u $service 查看日志。" + exit 1 + fi +done + +log_info "==========================================" +log_info "OpenStack 控制节点及 Keystone 初始化完成!" +log_info "控制节点:$CONTROLLER_HOSTNAME ($CONTROLLER_IP)" +log_info "MariaDB root 密码:123456,已允许远程登录。" +log_info "RabbitMQ 用户/密码:openstack / openstack" +log_info "Keystone 管理员/密码:admin / admin,区域:RegionOne" +log_info "Keystone API:http://controller:5000/v3" +log_info "==========================================" diff --git a/env3-3-local.sh b/env3-3-local.sh new file mode 100755 index 0000000..9c7e4e3 --- /dev/null +++ b/env3-3-local.sh @@ -0,0 +1,517 @@ +#!/bin/bash + +set -euo pipefail + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_err() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# 检查 root 权限 +if [ "$EUID" -ne 0 ]; then + log_err "请使用 root 权限执行此脚本(如: sudo bash $0)" + exit 1 +fi + +log_info "==========================================" +log_info "开始初始化实验环境..." +log_info "==========================================" + +# 直接连接终端,保留 yum 原生下载进度条;不要通过 tee 管道转存输出。 +run_yum() { + local status + log_info "执行 yum $*(下载速度取决于网络和镜像源)..." + # 仅启用本地仓库,避免系统中其他已启用的公网仓库参与下载。 + if yum --disablerepo='*' \ + --enablerepo=local-base,local-updates,local-extras,local-qemu-ev,local-openstack-rocky \ + "$@"; then + log_info "yum 操作完成。" + else + status=$? + log_err "yum 操作失败(退出码 $status),请查看上方错误信息。" + return "$status" + fi +} + +REPO_DIR="/etc/yum.repos.d" +BACKUP_DIR="/etc/yum.repos.d.bak" + +# 首次运行时备份原有软件源 +if [ ! -d "$BACKUP_DIR" ]; then + log_info "备份原有 yum 软件源..." + cp -a "$REPO_DIR" "$BACKUP_DIR" +fi + +# 配置 CentOS 7.9 基础源 +log_info "配置 CentOS 7.9 本地软件源(192.168.192.205:3080)..." + +cat > "$REPO_DIR/CentOS-Base.repo" <<'EOF' +[local-base] +name=Local - Base +baseurl=http://192.168.192.205:3080/vault-base/ +gpgcheck=0 +enabled=1 + +[local-updates] +name=Local - Updates +baseurl=http://192.168.192.205:3080/vault-updates/ +gpgcheck=0 +enabled=1 + +[local-extras] +name=Local - Extras +baseurl=http://192.168.192.205:3080/vault-extras/ +gpgcheck=0 +enabled=1 +EOF + +# 配置 QEMU EV 源 +cat > "$REPO_DIR/CentOS-QEMU-EV.repo" <<'EOF' +[local-qemu-ev] +name=Local - QEMU EV +baseurl=http://192.168.192.205:3080/vault-centos-qemu-ev/ +gpgcheck=0 +enabled=1 +EOF + +# 配置 OpenStack Rocky 源 +cat > "$REPO_DIR/CentOS-OpenStack-rocky.repo" <<'EOF' +[local-openstack-rocky] +name=Local - OpenStack Rocky +baseurl=http://192.168.192.205:3080/vault-centos-openstack-rocky/ +gpgcheck=0 +enabled=1 +exclude=sip,PyQt4 +EOF + +# 删除不再需要的 Ceph 源,避免 yum 读取其元数据 +rm -f "$REPO_DIR/CentOS-Ceph-Luminous.repo" + + +# 清除旧仓库元数据,保留已下载的软件包缓存 +log_info "刷新软件源元数据..." +run_yum clean metadata + +CONTROLLER_IP="192.168.30.129" +CONTROLLER_HOSTNAME="controller" + +# 首次修改时备份配置,重复运行不覆盖原始备份。 +backup_config() { + if [ -f "$1" ] && [ ! -e "$1.env3-3.bak" ]; then + cp -a "$1" "$1.env3-3.bak" + fi +} + +log_info "关闭防火墙和 SELinux..." +if systemctl list-unit-files firewalld.service --no-legend | grep -q '^firewalld.service'; then + systemctl stop firewalld + systemctl disable firewalld +fi +if command -v getenforce > /dev/null 2>&1; then + if [ "$(getenforce)" = "Enforcing" ]; then + setenforce 0 + fi +fi +if [ -f /etc/selinux/config ]; then + backup_config /etc/selinux/config + sed -i '/^[[:space:]]*SELINUX=/d' /etc/selinux/config + echo 'SELINUX=disabled' >> /etc/selinux/config +fi + +log_info "配置主机名和 hosts 解析..." +hostnamectl set-hostname "$CONTROLLER_HOSTNAME" +backup_config /etc/hosts +# 移除旧 controller 映射,保留同一行的其他别名及注释。 +HOSTS_TMP=$(mktemp) +awk -v host="$CONTROLLER_HOSTNAME" ' + /^[[:space:]]*#/ || NF == 0 { print; next } + { + comment = "" + pos = index($0, "#") + if (pos) { + comment = substr($0, pos) + $0 = substr($0, 1, pos - 1) + } + line = $1 + aliases = 0 + for (i = 2; i <= NF; i++) { + if ($i != host) { + line = line " " $i + aliases++ + } + } + if (aliases) print line (comment == "" ? "" : " " comment) + else if (comment != "") print comment + } +' /etc/hosts > "$HOSTS_TMP" +cat "$HOSTS_TMP" > /etc/hosts +rm -f "$HOSTS_TMP" +printf '%s %s\n' "$CONTROLLER_IP" "$CONTROLLER_HOSTNAME" >> /etc/hosts + +log_info "升级系统软件包,合并安装虚拟化、OpenStack 基础服务、Keystone 和 Glance..." +run_yum -y upgrade +run_yum install -y chrony python2-openstackclient \ + mariadb mariadb-server python2-PyMySQL \ + rabbitmq-server memcached python-memcached etcd \ + qemu-kvm libvirt virt-install bridge-utils qemu-img \ + openstack-keystone httpd mod_wsgi openstack-glance + +# 启动 libvirtd +log_info "启动 libvirtd 服务..." + +systemctl enable --now libvirtd > /dev/null + +# 验证服务状态 +if systemctl is-active --quiet libvirtd; then + log_info "libvirtd 状态正常。" +else + log_err "libvirtd 启动失败!" + exit 1 +fi + + +log_info "配置 chrony 时间服务器..." +backup_config /etc/chrony.conf +if ! grep -Eq '^[[:space:]]*allow[[:space:]]+all([[:space:]]*(#.*)?)?$' /etc/chrony.conf; then + printf '\n# 允许所有网段的机器访问同步时间\nallow all\n' >> /etc/chrony.conf +fi +systemctl enable chronyd +systemctl restart chronyd + +log_info "配置 MariaDB..." +mkdir -p /etc/my.cnf.d +backup_config /etc/my.cnf.d/openstack.cnf +cat > /etc/my.cnf.d/openstack.cnf <<'EOF' +[mysqld] +bind-address = 0.0.0.0 +# 默认使用 InnoDB,每个表的数据独立保存成文件。 +default-storage-engine = innodb +innodb_file_per_table = on +# 调高最大连接数,并使用 UTF-8 字符集。 +max_connections = 4096 +collation-server = utf8_general_ci +character-set-server = utf8 +EOF +systemctl enable mariadb +systemctl restart mariadb + +log_info "初始化 MariaDB 安全配置及 root 远程登录权限..." +# CentOS 7 的 MariaDB 5.5:自动执行 mysql_secure_installation 的等效操作。 +# 使用权限为 600 的临时客户端配置,避免把密码放进进程参数。 +MYSQL_AUTH_FILE=$(mktemp) +trap 'rm -f "$MYSQL_AUTH_FILE"' EXIT +chmod 600 "$MYSQL_AUTH_FILE" +cat > "$MYSQL_AUTH_FILE" <<'EOF' +[client] +user=root +password=123456 +protocol=socket +EOF +MYSQL_CMD=(mysql --defaults-extra-file="$MYSQL_AUTH_FILE") +if ! "${MYSQL_CMD[@]}" -e 'SELECT 1' > /dev/null 2>&1; then + MYSQL_CMD=(mysql --no-defaults --user=root --protocol=socket) + if ! "${MYSQL_CMD[@]}" -e 'SELECT 1' > /dev/null 2>&1; then + log_err "无法使用空密码或预设密码连接 MariaDB root,请检查已有数据库凭据。" + exit 1 + fi +fi +"${MYSQL_CMD[@]}" <<'SQL' +DELETE FROM mysql.user WHERE User = ''; +DELETE FROM mysql.user + WHERE User = 'root' AND Host NOT IN ('localhost', '127.0.0.1', '::1'); +UPDATE mysql.user SET Password = PASSWORD('123456') WHERE User = 'root'; +DROP DATABASE IF EXISTS test; +DELETE FROM mysql.db WHERE Db = 'test' OR Db = 'test\_%'; +FLUSH PRIVILEGES; +GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '123456' WITH GRANT OPTION; +FLUSH PRIVILEGES; +SQL +log_info "创建 Keystone 和 Glance 数据库并授权..." +mysql --defaults-extra-file="$MYSQL_AUTH_FILE" <<'SQL' +CREATE DATABASE IF NOT EXISTS keystone; +GRANT ALL PRIVILEGES ON keystone.* TO 'keystone'@'localhost' IDENTIFIED BY 'keystone'; +GRANT ALL PRIVILEGES ON keystone.* TO 'keystone'@'%' IDENTIFIED BY 'keystone'; +CREATE DATABASE IF NOT EXISTS glance; +GRANT ALL PRIVILEGES ON glance.* TO 'glance'@'localhost' IDENTIFIED BY 'glance'; +GRANT ALL PRIVILEGES ON glance.* TO 'glance'@'%' IDENTIFIED BY 'glance'; +FLUSH PRIVILEGES; +SQL +rm -f "$MYSQL_AUTH_FILE" +trap - EXIT + +log_info "配置 RabbitMQ 用户及权限..." +systemctl enable rabbitmq-server +systemctl start rabbitmq-server +RABBITMQ_USERS=$(rabbitmqctl list_users -q) +if awk '$1 == "openstack" { found = 1 } END { exit !found }' <<< "$RABBITMQ_USERS"; then + rabbitmqctl change_password openstack openstack +else + rabbitmqctl add_user openstack openstack +fi +rabbitmqctl set_permissions -p / openstack '.*' '.*' '.*' + +log_info "配置 Memcached..." +backup_config /etc/sysconfig/memcached +sed -i '/^[[:space:]]*OPTIONS=/d' /etc/sysconfig/memcached +echo 'OPTIONS="-l 127.0.0.1,::1,controller"' >> /etc/sysconfig/memcached +systemctl enable memcached +systemctl restart memcached + +log_info "配置 Etcd..." +backup_config /etc/etcd/etcd.conf +cat > /etc/etcd/etcd.conf <<'EOF' +#[Member] +ETCD_LISTEN_PEER_URLS="http://0.0.0.0:2380" +ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0:2379" +ETCD_NAME="controller" + +#[Clustering] +ETCD_INITIAL_ADVERTISE_PEER_URLS="http://controller:2380" +ETCD_ADVERTISE_CLIENT_URLS="http://controller:2379" +ETCD_INITIAL_CLUSTER="controller=http://controller:2380" +ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster-01" +ETCD_INITIAL_CLUSTER_STATE="new" +EOF +systemctl enable etcd +systemctl restart etcd + +log_info "配置 Keystone 数据库连接及 Fernet 令牌..." +backup_config /etc/keystone/keystone.conf +cat > /etc/keystone/keystone.conf <<'EOF' +[database] +connection = mysql+pymysql://keystone:keystone@controller/keystone +[token] +provider = fernet +EOF +chown root:keystone /etc/keystone/keystone.conf +chmod 640 /etc/keystone/keystone.conf + +log_info "同步 Keystone 数据库表结构..." +su -s /bin/sh -c "keystone-manage db_sync" keystone + +# 使用 setup 命令初始化密钥库,不删除已有密钥。 +log_info "初始化 Fernet 和 credential 密钥库..." +keystone-manage fernet_setup --keystone-user keystone --keystone-group keystone +keystone-manage credential_setup --keystone-user keystone --keystone-group keystone + +log_info "初始化 Keystone 管理员及服务端点..." +keystone-manage bootstrap --bootstrap-password admin \ + --bootstrap-admin-url http://controller:5000/v3 \ + --bootstrap-public-url http://controller:5000/v3 \ + --bootstrap-internal-url http://controller:5000/v3 \ + --bootstrap-region-id RegionOne + +log_info "配置 Apache HTTP 服务..." +backup_config /etc/httpd/conf/httpd.conf +# 删除已有生效的 ServerName,避免重复追加。 +sed -i '/^[[:space:]]*ServerName[[:space:]]/d' /etc/httpd/conf/httpd.conf +printf '\nServerName controller\n' >> /etc/httpd/conf/httpd.conf + +WSGI_SOURCE="/usr/share/keystone/wsgi-keystone.conf" +WSGI_LINK="/etc/httpd/conf.d/wsgi-keystone.conf" +if [ ! -f "$WSGI_SOURCE" ]; then + log_err "缺少 Keystone WSGI 配置:$WSGI_SOURCE" + exit 1 +fi +# 备份已有普通配置文件;重复运行时更新软链接。 +if [ ! -L "$WSGI_LINK" ]; then + backup_config "$WSGI_LINK" +fi +ln -sfnT "$WSGI_SOURCE" "$WSGI_LINK" + +log_info "检查 Apache 配置并启动服务..." +httpd -t +systemctl enable httpd +systemctl restart httpd + +log_info "创建并加载管理员登录脚本..." +ROOT_HOME=$(getent passwd root | cut -d: -f6) +if [ -z "$ROOT_HOME" ] || [ ! -d "$ROOT_HOME" ]; then + log_err "无法确定 root 用户的家目录。" + exit 1 +fi +ADMIN_OPENRC="$ROOT_HOME/admin-openstack.sh" +backup_config "$ADMIN_OPENRC" +cat > "$ADMIN_OPENRC" <<'EOF' +export OS_USERNAME=admin +export OS_PASSWORD=admin +export OS_PROJECT_NAME=admin +export OS_USER_DOMAIN_NAME=Default +export OS_PROJECT_DOMAIN_NAME=Default +export OS_AUTH_URL=http://controller:5000/v3 +export OS_IDENTITY_API_VERSION=3 +EOF +chmod 600 "$ADMIN_OPENRC" +chown root:root "$ADMIN_OPENRC" +# 清除继承的其他 OS_* 认证设置,避免学生之前的环境变量干扰本次配置。 +for variable in ${!OS_@}; do + unset "$variable" +done +source "$ADMIN_OPENRC" + +# Apache 启动后,等待 Keystone 实际可以签发令牌(不输出令牌内容)。 +log_info "等待 Keystone 管理员认证就绪..." +for attempt in {1..12}; do + if openstack --os-auth-type password --os-api-timeout 10 token issue > /dev/null; then + break + fi + if [ "$attempt" -eq 12 ]; then + log_err "Keystone 认证失败,请检查 httpd、Keystone 配置和管理员密码。" + exit 1 + fi + sleep 2 +done + +# 查询失败直接停止;只有成功查询且没有匹配项时才创建。 +# 对已有重复资源报错,避免任意选择或继续制造重复项。 +unique_resource_id() { + python -c ' +import json, sys +field, expected = sys.argv[1:3] +rows = json.load(sys.stdin) +ids = [str(row["ID"]) for row in rows + if field == "*" or row.get(field) == expected] +if len(ids) > 1: + sys.stderr.write("Found duplicate resources: %s\n" % ", ".join(ids)) + sys.exit(1) +if ids: + print(ids[0]) +' "$1" "$2" +} + +log_info "创建或复用 service 项目、glance 用户及服务..." +SERVICE_PROJECT_ID=$(openstack project list --domain default -f json | unique_resource_id Name service) +if [ -z "$SERVICE_PROJECT_ID" ]; then + SERVICE_PROJECT_ID=$(openstack project create --domain default service -f value -c id) +fi +openstack project set --enable "$SERVICE_PROJECT_ID" + +GLANCE_USER_ID=$(openstack user list --domain default -f json | unique_resource_id Name glance) +if [ -z "$GLANCE_USER_ID" ]; then + GLANCE_USER_ID=$(openstack user create --domain default --password glance glance -f value -c id) +else + openstack user set --password glance --enable "$GLANCE_USER_ID" +fi +# Keystone 的角色授权可重复提交,不会生成重复授权。 +openstack role add --project "$SERVICE_PROJECT_ID" --user "$GLANCE_USER_ID" admin + +GLANCE_SERVICE_ID=$(openstack service list -f json | unique_resource_id Name glance) +if [ -z "$GLANCE_SERVICE_ID" ]; then + GLANCE_SERVICE_ID=$(openstack service create --name glance --description "OpenStack Image" image -f value -c id) +else + openstack service set --type image --description "OpenStack Image" --enable "$GLANCE_SERVICE_ID" +fi + +log_info "创建或更新 Glance API 端点..." +for interface in public internal admin; do + ENDPOINT_ID=$(openstack endpoint list --service "$GLANCE_SERVICE_ID" \ + --region RegionOne --interface "$interface" -f json | unique_resource_id '*' '') + if [ -z "$ENDPOINT_ID" ]; then + openstack endpoint create --region RegionOne \ + "$GLANCE_SERVICE_ID" "$interface" http://controller:9292 + else + openstack endpoint set --url http://controller:9292 --enable "$ENDPOINT_ID" + fi +done + +log_info "配置 Glance API 和 Registry..." +backup_config /etc/glance/glance-api.conf +backup_config /etc/glance/glance-registry.conf +# 两个服务共用数据库和认证设置,一次生成后复用。 +cat > /etc/glance/glance-registry.conf <<'EOF' +[database] +connection = mysql+pymysql://glance:glance@controller/glance + +[keystone_authtoken] +www_authenticate_uri = http://controller:5000 +auth_url = http://controller:5000 +memcached_servers = controller:11211 +auth_type = password +project_domain_name = Default +user_domain_name = Default +project_name = service +username = glance +password = glance + +[paste_deploy] +flavor = keystone +EOF +cat /etc/glance/glance-registry.conf > /etc/glance/glance-api.conf +cat >> /etc/glance/glance-api.conf <<'EOF' + +[glance_store] +stores = file,http +default_store = file +filesystem_store_datadir = /var/lib/glance/images/ +EOF +chown root:glance /etc/glance/glance-api.conf /etc/glance/glance-registry.conf +chmod 640 /etc/glance/glance-api.conf /etc/glance/glance-registry.conf +# 保留已有镜像文件,只确保目录存在且 Glance 可写。 +install -d -o glance -g glance -m 0750 /var/lib/glance/images + +log_info "同步 Glance 数据库并验证表结构..." +su -s /bin/sh -c "glance-manage db_sync" glance +# 以 glance 用户连接数据库,验证同步后的核心表,无需再次创建 root 凭据文件。 +python - <<'PYDB' +import pymysql +conn = pymysql.connect(host='controller', user='glance', password='glance', database='glance') +try: + with conn.cursor() as cursor: + cursor.execute('SHOW TABLES') + tables = [row[0] for row in cursor.fetchall()] + if 'images' not in tables: + raise RuntimeError('Glance images table is missing') + print('Glance tables: ' + ', '.join(tables)) +finally: + conn.close() +PYDB + +log_info "启动 Glance 服务并设置开机自启..." +systemctl daemon-reload +systemctl enable openstack-glance-api openstack-glance-registry +systemctl restart openstack-glance-api openstack-glance-registry + +log_info "验证 Glance API 和管理员访问..." +for attempt in {1..12}; do + if openstack --os-api-timeout 10 --os-image-api-version 2 image list; then + break + fi + if [ "$attempt" -eq 12 ]; then + log_err "Glance API 验证失败,请检查 /var/log/glance/ 中的日志。" + exit 1 + fi + sleep 2 +done + +log_info "检查各项服务状态..." +for service in libvirtd chronyd mariadb rabbitmq-server memcached etcd httpd openstack-glance-api openstack-glance-registry; do + if systemctl is-active --quiet "$service"; then + log_info "$service 状态正常。" + else + log_err "$service 未正常运行,请使用 journalctl -u $service 查看日志。" + exit 1 + fi +done + +log_info "==========================================" +log_info "OpenStack 控制节点、Keystone 和 Glance 初始化完成!" +log_info "控制节点:$CONTROLLER_HOSTNAME ($CONTROLLER_IP)" +log_info "MariaDB root 密码:123456,已允许远程登录。" +log_info "RabbitMQ 用户/密码:openstack / openstack" +log_info "Keystone 管理员/密码:admin / admin,区域:RegionOne" +log_info "Keystone API:http://controller:5000/v3" +log_info "Glance API:http://controller:9292" +log_info "后续在当前终端使用 OpenStack 命令前,请执行:source $ADMIN_OPENRC" +log_info "==========================================" diff --git a/env3-4-local.sh b/env3-4-local.sh new file mode 100755 index 0000000..ef307e7 --- /dev/null +++ b/env3-4-local.sh @@ -0,0 +1,707 @@ +#!/bin/bash + +# CentOS 7.9 / OpenStack Rocky 控制节点完整初始化脚本。 +# 包含 env3-3 的基础环境、Keystone、Glance,并增加 Nova / Placement。 +# 在控制节点以 root 执行;运行前请核对 CONTROLLER_IP 和本地仓库地址。 +set -euo pipefail + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_err() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# 检查 root 权限 +if [ "$EUID" -ne 0 ]; then + log_err "请使用 root 权限执行此脚本(如: sudo bash $0)" + exit 1 +fi + +log_info "==========================================" +log_info "开始初始化实验环境..." +log_info "==========================================" + +# 直接连接终端,保留 yum 原生下载进度条;不要通过 tee 管道转存输出。 +run_yum() { + local status + log_info "执行 yum $*(下载速度取决于网络和镜像源)..." + # 仅启用本地仓库,避免系统中其他已启用的公网仓库参与下载。 + if yum --disablerepo='*' \ + --enablerepo=local-base,local-updates,local-extras,local-qemu-ev,local-openstack-rocky \ + "$@"; then + log_info "yum 操作完成。" + else + status=$? + log_err "yum 操作失败(退出码 $status),请查看上方错误信息。" + return "$status" + fi +} + +REPO_DIR="/etc/yum.repos.d" +BACKUP_DIR="/etc/yum.repos.d.bak" + +# 首次运行时备份原有软件源 +if [ ! -d "$BACKUP_DIR" ]; then + log_info "备份原有 yum 软件源..." + cp -a "$REPO_DIR" "$BACKUP_DIR" +fi + +# 配置 CentOS 7.9 基础源 +log_info "配置 CentOS 7.9 本地软件源(192.168.192.205:3080)..." + +cat > "$REPO_DIR/CentOS-Base.repo" <<'EOF' +[local-base] +name=Local - Base +baseurl=http://192.168.192.205:3080/vault-base/ +gpgcheck=0 +enabled=1 + +[local-updates] +name=Local - Updates +baseurl=http://192.168.192.205:3080/vault-updates/ +gpgcheck=0 +enabled=1 + +[local-extras] +name=Local - Extras +baseurl=http://192.168.192.205:3080/vault-extras/ +gpgcheck=0 +enabled=1 +EOF + +# 配置 QEMU EV 源 +cat > "$REPO_DIR/CentOS-QEMU-EV.repo" <<'EOF' +[local-qemu-ev] +name=Local - QEMU EV +baseurl=http://192.168.192.205:3080/vault-centos-qemu-ev/ +gpgcheck=0 +enabled=1 +EOF + +# 配置 OpenStack Rocky 源 +cat > "$REPO_DIR/CentOS-OpenStack-rocky.repo" <<'EOF' +[local-openstack-rocky] +name=Local - OpenStack Rocky +baseurl=http://192.168.192.205:3080/vault-centos-openstack-rocky/ +gpgcheck=0 +enabled=1 +exclude=sip,PyQt4 +EOF + +# 删除不再需要的 Ceph 源,避免 yum 读取其元数据 +rm -f "$REPO_DIR/CentOS-Ceph-Luminous.repo" + + +# 清除旧仓库元数据,保留已下载的软件包缓存 +log_info "刷新软件源元数据..." +run_yum clean metadata + +CONTROLLER_IP="192.168.30.129" +CONTROLLER_HOSTNAME="controller" + +# 首次修改时备份配置,重复运行不覆盖原始备份。 +backup_config() { + if [ -f "$1" ] && [ ! -e "$1.env3-4.bak" ]; then + cp -a "$1" "$1.env3-4.bak" + fi +} + +log_info "关闭防火墙和 SELinux..." +if systemctl list-unit-files firewalld.service --no-legend | grep -q '^firewalld.service'; then + systemctl stop firewalld + systemctl disable firewalld +fi +if command -v getenforce > /dev/null 2>&1; then + if [ "$(getenforce)" = "Enforcing" ]; then + setenforce 0 + fi +fi +if [ -f /etc/selinux/config ]; then + backup_config /etc/selinux/config + sed -i '/^[[:space:]]*SELINUX=/d' /etc/selinux/config + echo 'SELINUX=disabled' >> /etc/selinux/config +fi + +log_info "配置主机名和 hosts 解析..." +hostnamectl set-hostname "$CONTROLLER_HOSTNAME" +backup_config /etc/hosts +# 移除旧 controller 映射,保留同一行的其他别名及注释。 +HOSTS_TMP=$(mktemp) +awk -v host="$CONTROLLER_HOSTNAME" ' + /^[[:space:]]*#/ || NF == 0 { print; next } + { + comment = "" + pos = index($0, "#") + if (pos) { + comment = substr($0, pos) + $0 = substr($0, 1, pos - 1) + } + line = $1 + aliases = 0 + for (i = 2; i <= NF; i++) { + if ($i != host) { + line = line " " $i + aliases++ + } + } + if (aliases) print line (comment == "" ? "" : " " comment) + else if (comment != "") print comment + } +' /etc/hosts > "$HOSTS_TMP" +cat "$HOSTS_TMP" > /etc/hosts +rm -f "$HOSTS_TMP" +printf '%s %s\n' "$CONTROLLER_IP" "$CONTROLLER_HOSTNAME" >> /etc/hosts + +log_info "升级系统软件包,合并安装基础服务、Keystone、Glance、Nova 和 Placement..." +run_yum -y upgrade +run_yum install -y chrony python2-openstackclient \ + mariadb mariadb-server python2-PyMySQL \ + rabbitmq-server memcached python-memcached etcd \ + qemu-kvm libvirt virt-install bridge-utils qemu-img \ + openstack-keystone httpd mod_wsgi openstack-glance \ + openstack-nova-api openstack-nova-conductor openstack-nova-console \ + openstack-nova-novncproxy openstack-nova-scheduler openstack-nova-placement-api + +# 启动 libvirtd +log_info "启动 libvirtd 服务..." + +systemctl enable --now libvirtd > /dev/null + +# 验证服务状态 +if systemctl is-active --quiet libvirtd; then + log_info "libvirtd 状态正常。" +else + log_err "libvirtd 启动失败!" + exit 1 +fi + + +log_info "配置 chrony 时间服务器..." +backup_config /etc/chrony.conf +if ! grep -Eq '^[[:space:]]*allow[[:space:]]+all([[:space:]]*(#.*)?)?$' /etc/chrony.conf; then + printf '\n# 允许所有网段的机器访问同步时间\nallow all\n' >> /etc/chrony.conf +fi +systemctl enable chronyd +systemctl restart chronyd + +log_info "配置 MariaDB..." +mkdir -p /etc/my.cnf.d +backup_config /etc/my.cnf.d/openstack.cnf +cat > /etc/my.cnf.d/openstack.cnf <<'EOF' +[mysqld] +bind-address = 0.0.0.0 +# 默认使用 InnoDB,每个表的数据独立保存成文件。 +default-storage-engine = innodb +innodb_file_per_table = on +# 调高最大连接数,并使用 UTF-8 字符集。 +max_connections = 4096 +collation-server = utf8_general_ci +character-set-server = utf8 +EOF +systemctl enable mariadb +systemctl restart mariadb + +log_info "初始化 MariaDB 安全配置及 root 远程登录权限..." +# CentOS 7 的 MariaDB 5.5:自动执行 mysql_secure_installation 的等效操作。 +# 使用权限为 600 的临时客户端配置,避免把密码放进进程参数。 +MYSQL_AUTH_FILE=$(mktemp) +trap 'rm -f "$MYSQL_AUTH_FILE"' EXIT +chmod 600 "$MYSQL_AUTH_FILE" +cat > "$MYSQL_AUTH_FILE" <<'EOF' +[client] +user=root +password=123456 +protocol=socket +EOF +MYSQL_CMD=(mysql --defaults-extra-file="$MYSQL_AUTH_FILE") +if ! "${MYSQL_CMD[@]}" -e 'SELECT 1' > /dev/null 2>&1; then + MYSQL_CMD=(mysql --no-defaults --user=root --protocol=socket) + if ! "${MYSQL_CMD[@]}" -e 'SELECT 1' > /dev/null 2>&1; then + log_err "无法使用空密码或预设密码连接 MariaDB root,请检查已有数据库凭据。" + exit 1 + fi +fi +"${MYSQL_CMD[@]}" <<'SQL' +DELETE FROM mysql.user WHERE User = ''; +DELETE FROM mysql.user + WHERE User = 'root' AND Host NOT IN ('localhost', '127.0.0.1', '::1'); +UPDATE mysql.user SET Password = PASSWORD('123456') WHERE User = 'root'; +DROP DATABASE IF EXISTS test; +DELETE FROM mysql.db WHERE Db = 'test' OR Db = 'test\_%'; +FLUSH PRIVILEGES; +GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '123456' WITH GRANT OPTION; +FLUSH PRIVILEGES; +SQL +log_info "批量创建 Keystone、Glance、Nova 和 Placement 数据库并授权..." +mysql --defaults-extra-file="$MYSQL_AUTH_FILE" <<'SQL' +CREATE DATABASE IF NOT EXISTS keystone; +GRANT ALL PRIVILEGES ON keystone.* TO 'keystone'@'localhost' IDENTIFIED BY 'keystone'; +GRANT ALL PRIVILEGES ON keystone.* TO 'keystone'@'%' IDENTIFIED BY 'keystone'; +CREATE DATABASE IF NOT EXISTS glance; +GRANT ALL PRIVILEGES ON glance.* TO 'glance'@'localhost' IDENTIFIED BY 'glance'; +GRANT ALL PRIVILEGES ON glance.* TO 'glance'@'%' IDENTIFIED BY 'glance'; +CREATE DATABASE IF NOT EXISTS nova_api; +CREATE DATABASE IF NOT EXISTS nova; +CREATE DATABASE IF NOT EXISTS nova_cell0; +CREATE DATABASE IF NOT EXISTS placement; +GRANT ALL PRIVILEGES ON nova_api.* TO 'nova'@'localhost' IDENTIFIED BY 'nova'; +GRANT ALL PRIVILEGES ON nova_api.* TO 'nova'@'%' IDENTIFIED BY 'nova'; +GRANT ALL PRIVILEGES ON nova.* TO 'nova'@'localhost' IDENTIFIED BY 'nova'; +GRANT ALL PRIVILEGES ON nova.* TO 'nova'@'%' IDENTIFIED BY 'nova'; +GRANT ALL PRIVILEGES ON nova_cell0.* TO 'nova'@'localhost' IDENTIFIED BY 'nova'; +GRANT ALL PRIVILEGES ON nova_cell0.* TO 'nova'@'%' IDENTIFIED BY 'nova'; +GRANT ALL PRIVILEGES ON placement.* TO 'placement'@'localhost' IDENTIFIED BY 'placement'; +GRANT ALL PRIVILEGES ON placement.* TO 'placement'@'%' IDENTIFIED BY 'placement'; +FLUSH PRIVILEGES; +SQL +rm -f "$MYSQL_AUTH_FILE" +trap - EXIT + +log_info "配置 RabbitMQ 用户及权限..." +systemctl enable rabbitmq-server +systemctl start rabbitmq-server +RABBITMQ_USERS=$(rabbitmqctl list_users -q) +if awk '$1 == "openstack" { found = 1 } END { exit !found }' <<< "$RABBITMQ_USERS"; then + rabbitmqctl change_password openstack openstack +else + rabbitmqctl add_user openstack openstack +fi +rabbitmqctl set_permissions -p / openstack '.*' '.*' '.*' + +log_info "配置 Memcached..." +backup_config /etc/sysconfig/memcached +sed -i '/^[[:space:]]*OPTIONS=/d' /etc/sysconfig/memcached +echo 'OPTIONS="-l 127.0.0.1,::1,controller"' >> /etc/sysconfig/memcached +systemctl enable memcached +systemctl restart memcached + +log_info "配置 Etcd..." +backup_config /etc/etcd/etcd.conf +cat > /etc/etcd/etcd.conf <<'EOF' +#[Member] +ETCD_LISTEN_PEER_URLS="http://0.0.0.0:2380" +ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0:2379" +ETCD_NAME="controller" + +#[Clustering] +ETCD_INITIAL_ADVERTISE_PEER_URLS="http://controller:2380" +ETCD_ADVERTISE_CLIENT_URLS="http://controller:2379" +ETCD_INITIAL_CLUSTER="controller=http://controller:2380" +ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster-01" +ETCD_INITIAL_CLUSTER_STATE="new" +EOF +systemctl enable etcd +systemctl restart etcd + +log_info "配置 Keystone 数据库连接及 Fernet 令牌..." +backup_config /etc/keystone/keystone.conf +cat > /etc/keystone/keystone.conf <<'EOF' +[database] +connection = mysql+pymysql://keystone:keystone@controller/keystone +[token] +provider = fernet +EOF +chown root:keystone /etc/keystone/keystone.conf +chmod 640 /etc/keystone/keystone.conf + +log_info "同步 Keystone 数据库表结构..." +su -s /bin/sh -c "keystone-manage db_sync" keystone + +# 使用 setup 命令初始化密钥库,不删除已有密钥。 +log_info "初始化 Fernet 和 credential 密钥库..." +keystone-manage fernet_setup --keystone-user keystone --keystone-group keystone +keystone-manage credential_setup --keystone-user keystone --keystone-group keystone + +log_info "初始化 Keystone 管理员及服务端点..." +keystone-manage bootstrap --bootstrap-password admin \ + --bootstrap-admin-url http://controller:5000/v3 \ + --bootstrap-public-url http://controller:5000/v3 \ + --bootstrap-internal-url http://controller:5000/v3 \ + --bootstrap-region-id RegionOne + +log_info "配置 Apache HTTP 服务..." +backup_config /etc/httpd/conf/httpd.conf +# 删除已有生效的 ServerName,避免重复追加。 +sed -i '/^[[:space:]]*ServerName[[:space:]]/d' /etc/httpd/conf/httpd.conf +printf '\nServerName controller\n' >> /etc/httpd/conf/httpd.conf + +WSGI_SOURCE="/usr/share/keystone/wsgi-keystone.conf" +WSGI_LINK="/etc/httpd/conf.d/wsgi-keystone.conf" +if [ ! -f "$WSGI_SOURCE" ]; then + log_err "缺少 Keystone WSGI 配置:$WSGI_SOURCE" + exit 1 +fi +# 备份已有普通配置文件;重复运行时更新软链接。 +if [ ! -L "$WSGI_LINK" ]; then + backup_config "$WSGI_LINK" +fi +ln -sfnT "$WSGI_SOURCE" "$WSGI_LINK" + +log_info "配置 Nova 和 Placement..." +backup_config /etc/nova/nova.conf +# 仅展开控制节点 IP;保留 $my_ip 供 Nova 配置系统引用。 +cat > /etc/nova/nova.conf <> "$PLACEMENT_HTTPD_CONF" <<'EOF' +# BEGIN env3-4 placement access + + = 2.4> + Require all granted + + + Order allow,deny + Allow from all + + +# END env3-4 placement access +EOF + +log_info "检查 Apache 配置,合并应用 Keystone 和 Placement 配置..." +httpd -t +systemctl enable httpd +systemctl restart httpd + +log_info "创建并加载管理员登录脚本..." +ROOT_HOME=$(getent passwd root | cut -d: -f6) +if [ -z "$ROOT_HOME" ] || [ ! -d "$ROOT_HOME" ]; then + log_err "无法确定 root 用户的家目录。" + exit 1 +fi +ADMIN_OPENRC="$ROOT_HOME/admin-openstack.sh" +backup_config "$ADMIN_OPENRC" +cat > "$ADMIN_OPENRC" <<'EOF' +export OS_USERNAME=admin +export OS_PASSWORD=admin +export OS_PROJECT_NAME=admin +export OS_USER_DOMAIN_NAME=Default +export OS_PROJECT_DOMAIN_NAME=Default +export OS_AUTH_URL=http://controller:5000/v3 +export OS_IDENTITY_API_VERSION=3 +EOF +chmod 600 "$ADMIN_OPENRC" +chown root:root "$ADMIN_OPENRC" +# 清除继承的其他 OS_* 认证设置,避免学生之前的环境变量干扰本次配置。 +for variable in ${!OS_@}; do + unset "$variable" +done +source "$ADMIN_OPENRC" + +# Apache 启动后,等待 Keystone 实际可以签发令牌(不输出令牌内容)。 +log_info "等待 Keystone 管理员认证就绪..." +for attempt in {1..12}; do + if openstack --os-auth-type password --os-api-timeout 10 token issue > /dev/null; then + break + fi + if [ "$attempt" -eq 12 ]; then + log_err "Keystone 认证失败,请检查 httpd、Keystone 配置和管理员密码。" + exit 1 + fi + sleep 2 +done + +# 查询失败直接停止;只有成功查询且没有匹配项时才创建。 +# 对已有重复资源报错,避免任意选择或继续制造重复项。 +unique_resource_id() { + python -c ' +import json, sys +field, expected = sys.argv[1:3] +rows = json.load(sys.stdin) +ids = [str(row["ID"]) for row in rows + if field == "*" or row.get(field) == expected] +if len(ids) > 1: + sys.stderr.write("Found duplicate resources: %s\n" % ", ".join(ids)) + sys.exit(1) +if ids: + print(ids[0]) +' "$1" "$2" +} + +log_info "创建或复用 service 项目及 Glance、Nova、Placement 服务凭证..." +SERVICE_PROJECT_ID=$(openstack project list --domain default -f json | unique_resource_id Name service) +if [ -z "$SERVICE_PROJECT_ID" ]; then + SERVICE_PROJECT_ID=$(openstack project create --domain default service -f value -c id) +fi +openstack project set --enable "$SERVICE_PROJECT_ID" + +# 用户和服务清单各查询一次;三个不同名称无需重复获取完整清单。 +SERVICE_USERS=$(openstack user list --domain default -f json) +SERVICE_CATALOG=$(openstack service list -f json) +for service_name in glance nova placement; do + case "$service_name" in + glance) + service_type=image + service_description="OpenStack Image" + service_url=http://controller:9292 + ;; + nova) + service_type=compute + service_description="OpenStack Compute" + service_url=http://controller:8774/v2.1 + ;; + placement) + service_type=placement + service_description="Placement API" + service_url=http://controller:8778 + ;; + esac + USER_ID=$(unique_resource_id Name "$service_name" <<< "$SERVICE_USERS") + if [ -z "$USER_ID" ]; then + USER_ID=$(openstack user create --domain default --password "$service_name" \ + "$service_name" -f value -c id) + else + openstack user set --password "$service_name" --enable "$USER_ID" + fi + # Keystone 的角色授权可重复提交,不会生成重复授权。 + openstack role add --project "$SERVICE_PROJECT_ID" --user "$USER_ID" admin + + SERVICE_ID=$(unique_resource_id Name "$service_name" <<< "$SERVICE_CATALOG") + if [ -z "$SERVICE_ID" ]; then + SERVICE_ID=$(openstack service create --name "$service_name" \ + --description "$service_description" "$service_type" -f value -c id) + else + openstack service set --type "$service_type" --description "$service_description" \ + --enable "$SERVICE_ID" + fi + + log_info "创建或更新 $service_name API 的三个端点..." + # 每个服务只查询一次端点列表,按接口筛选,避免反复连接 Keystone。 + SERVICE_ENDPOINTS=$(openstack endpoint list --service "$SERVICE_ID" --region RegionOne -f json) + for interface in public internal admin; do + ENDPOINT_ID=$(unique_resource_id Interface "$interface" <<< "$SERVICE_ENDPOINTS") + if [ -z "$ENDPOINT_ID" ]; then + openstack endpoint create --region RegionOne "$SERVICE_ID" "$interface" "$service_url" + else + openstack endpoint set --url "$service_url" --enable "$ENDPOINT_ID" + fi + done +done + +log_info "配置 Glance API 和 Registry..." +backup_config /etc/glance/glance-api.conf +backup_config /etc/glance/glance-registry.conf +# 两个服务共用数据库和认证设置,一次生成后复用。 +cat > /etc/glance/glance-registry.conf <<'EOF' +[database] +connection = mysql+pymysql://glance:glance@controller/glance + +[keystone_authtoken] +www_authenticate_uri = http://controller:5000 +auth_url = http://controller:5000 +memcached_servers = controller:11211 +auth_type = password +project_domain_name = Default +user_domain_name = Default +project_name = service +username = glance +password = glance + +[paste_deploy] +flavor = keystone +EOF +cat /etc/glance/glance-registry.conf > /etc/glance/glance-api.conf +cat >> /etc/glance/glance-api.conf <<'EOF' + +[glance_store] +stores = file,http +default_store = file +filesystem_store_datadir = /var/lib/glance/images/ +EOF +chown root:glance /etc/glance/glance-api.conf /etc/glance/glance-registry.conf +chmod 640 /etc/glance/glance-api.conf /etc/glance/glance-registry.conf +# 保留已有镜像文件,只确保目录存在且 Glance 可写。 +install -d -o glance -g glance -m 0750 /var/lib/glance/images + +log_info "同步 Glance 数据库并验证表结构..." +su -s /bin/sh -c "glance-manage db_sync" glance +# 以 glance 用户连接数据库,验证同步后的核心表,无需再次创建 root 凭据文件。 +python - <<'PYDB' +import pymysql +conn = pymysql.connect(host='controller', user='glance', password='glance', database='glance') +try: + with conn.cursor() as cursor: + cursor.execute('SHOW TABLES') + tables = [row[0] for row in cursor.fetchall()] + if 'images' not in tables: + raise RuntimeError('Glance images table is missing') + print('Glance tables: ' + ', '.join(tables)) +finally: + conn.close() +PYDB + +log_info "同步 Nova API / Placement 数据库并注册 Cells v2..." +# Rocky 的 api_db sync 同时同步 [placement_database] 指定的数据库。 +su -s /bin/sh -c "nova-manage api_db sync" nova +su -s /bin/sh -c "nova-manage cell_v2 map_cell0" nova +CELL_LIST=$(su -s /bin/sh -c "nova-manage cell_v2 list_cells" nova) +CELL1_COUNT=$(awk -F '|' '$2 ~ /^[[:space:]]*cell1[[:space:]]*$/ { count++ } END { print count+0 }' <<< "$CELL_LIST") +case "$CELL1_COUNT" in + 0) su -s /bin/sh -c "nova-manage cell_v2 create_cell --name=cell1 --verbose" nova ;; + 1) log_info "cell1 已存在,复用已有映射。" ;; + *) log_err "发现多个 cell1,请先检查 cell 映射,避免重复注册。"; exit 1 ;; +esac +su -s /bin/sh -c "nova-manage db sync" nova +su -s /bin/sh -c "nova-manage cell_v2 list_cells" nova + +log_info "验证 Nova / Placement 四个数据库的表结构和 cell 映射..." +python - <<'PYDB' +import pymysql + +databases = [ + ('nova_api', 'nova', 'cell_mappings'), + ('nova', 'nova', 'instances'), + ('nova_cell0', 'nova', 'instances'), + ('placement', 'placement', 'resource_providers'), +] +for database, user, required_table in databases: + conn = pymysql.connect(host='controller', user=user, password=user, database=database) + try: + with conn.cursor() as cursor: + cursor.execute('SHOW TABLES') + tables = [row[0] for row in cursor.fetchall()] + if required_table not in tables: + raise RuntimeError('%s: missing table %s' % (database, required_table)) + print('%s tables: %s' % (database, ', '.join(tables))) + if database == 'nova_api': + cursor.execute('SELECT name, uuid, database_connection, transport_url, disabled ' + 'FROM cell_mappings') + cells = cursor.fetchall() + expected = { + 'cell0': ('mysql+pymysql://nova:nova@controller/nova_cell0', 'none:///'), + 'cell1': ('mysql+pymysql://nova:nova@controller/nova', + 'rabbit://openstack:openstack@controller'), + } + for name, (db_url, transport) in expected.items(): + matches = [row for row in cells if row[0] == name] + if len(matches) != 1: + raise RuntimeError('Expected exactly one %s mapping' % name) + row = matches[0] + if row[2] != db_url or row[3] != transport or row[4]: + raise RuntimeError('%s mapping differs from the lab configuration' % name) + if name == 'cell0' and row[1] != '00000000-0000-0000-0000-000000000000': + raise RuntimeError('Invalid cell0 UUID') + print('%s mapping OK: %s' % (name, row[1])) + finally: + conn.close() +PYDB + +log_info "批量启动 Glance、Nova 服务并设置开机自启..." +NOVA_SERVICES=( + openstack-nova-api + openstack-nova-consoleauth + openstack-nova-scheduler + openstack-nova-conductor + openstack-nova-novncproxy +) +systemctl daemon-reload +systemctl enable openstack-glance-api openstack-glance-registry "${NOVA_SERVICES[@]}" +# restart 同时支持首次启动和重跑后加载新配置。 +systemctl restart openstack-glance-api openstack-glance-registry "${NOVA_SERVICES[@]}" + +log_info "验证 Glance API 和管理员访问..." +for attempt in {1..12}; do + if openstack --os-api-timeout 10 --os-image-api-version 2 image list; then + break + fi + if [ "$attempt" -eq 12 ]; then + log_err "Glance API 验证失败,请检查 /var/log/glance/ 中的日志。" + exit 1 + fi + sleep 2 +done + +log_info "检查各项服务状态..." +for service in libvirtd chronyd mariadb rabbitmq-server memcached etcd httpd \ + openstack-glance-api openstack-glance-registry "${NOVA_SERVICES[@]}"; do + if systemctl is-active --quiet "$service"; then + log_info "$service 状态正常。" + else + log_err "$service 未正常运行,请使用 journalctl -u $service 查看日志。" + exit 1 + fi +done + +log_info "==========================================" +log_info "OpenStack 控制节点、Keystone、Glance、Nova 和 Placement 初始化完成!" +log_info "控制节点:$CONTROLLER_HOSTNAME ($CONTROLLER_IP)" +log_info "MariaDB root 密码:123456,已允许远程登录。" +log_info "RabbitMQ 用户/密码:openstack / openstack" +log_info "Keystone 管理员/密码:admin / admin,区域:RegionOne" +log_info "Keystone API:http://controller:5000/v3" +log_info "Glance API:http://controller:9292" +log_info "Nova API:http://controller:8774/v2.1,用户/密码:nova / nova" +log_info "Placement API:http://controller:8778,用户/密码:placement / placement" +log_info "后续在当前终端使用 OpenStack 命令前,请执行:source $ADMIN_OPENRC" +log_info "=========================================="