Files

749 lines
27 KiB
Bash
Executable File
Raw Permalink 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
# 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 <<EOF
[DEFAULT]
my_ip = $CONTROLLER_IP
use_neutron = true
firewall_driver = nova.virt.firewall.NoopFirewallDriver
enabled_apis = osapi_compute,metadata
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
memcached_servers = controller:11211
auth_type = password
project_domain_name = Default
user_domain_name = Default
project_name = service
username = nova
password = nova
[vnc]
enabled = true
server_listen = \$my_ip
server_proxyclient_address = \$my_ip
[glance]
api_servers = http://controller:9292
[oslo_concurrency]
lock_path = /var/lib/nova/tmp
[placement]
region_name = RegionOne
project_domain_name = Default
project_name = service
auth_type = password
user_domain_name = Default
auth_url = http://controller:5000/v3
username = placement
password = placement
EOF
chown root:nova /etc/nova/nova.conf
chmod 640 /etc/nova/nova.conf
install -d -o nova -g nova -m 0750 /var/lib/nova/tmp
log_info "修复 Placement API 的 Apache 目录访问权限..."
PLACEMENT_HTTPD_CONF=/etc/httpd/conf.d/00-nova-placement-api.conf
if [ ! -f "$PLACEMENT_HTTPD_CONF" ]; then
log_err "缺少 Placement WSGI 配置:$PLACEMENT_HTTPD_CONF,请检查软件包安装。"
exit 1
fi
backup_config "$PLACEMENT_HTTPD_CONF"
# 兼容旧脚本:原配置末尾缺少换行时,块标记可能粘在 </Location> 等标签后。
# 先拆开粘连的标记,确保下面的范围删除能完整清理旧权限块。
sed -i 's/># BEGIN env3-4 placement access$/>\
# BEGIN env3-4 placement access/' "$PLACEMENT_HTTPD_CONF"
# 只替换本脚本管理的配置块,保留软件包提供的 VirtualHost / WSGI 配置。
sed -i '/^# BEGIN env3-4 placement access$/,/^# END env3-4 placement access$/d' "$PLACEMENT_HTTPD_CONF"
# 显式分隔原配置与追加块,避免无末尾换行的配置产生 </Location># 等错误。
printf '\n' >> "$PLACEMENT_HTTPD_CONF"
cat >> "$PLACEMENT_HTTPD_CONF" <<'EOF'
# BEGIN env3-4 placement access
<Directory /usr/bin>
<IfVersion >= 2.4>
Require all granted
</IfVersion>
<IfVersion < 2.4>
Order allow,deny
Allow from all
</IfVersion>
</Directory>
# 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"
# 认证失败不一定是密码错误;输出连接和 WSGI 诊断,避免只留下统一提示。
diagnose_keystone() {
local url logfile
log_warn "Keystone 诊断:controller 解析和本机 IPv4 地址"
getent hosts controller || true
ip -4 -o addr show || true
log_warn "Keystone 诊断:Apache 虚拟主机配置"
httpd -S 2>&1 || true
if command -v curl > /dev/null 2>&1; then
# 无凭证探测,绕过代理以区分本机 WSGI 故障与主机名/网络问题。
for url in http://127.0.0.1:5000/v3/ http://controller:5000/v3/; do
log_info "直接探测 $url"
curl --noproxy '*' \
-sS -o /dev/null -w 'HTTP 状态码:%{http_code}\n' "$url" || true
done
fi
log_warn "Keystone 诊断:最近的 httpd 服务日志"
journalctl -u httpd -n 30 --no-pager 2>&1 || true
for logfile in /var/log/httpd/error_log /var/log/httpd/*error*.log \
/var/log/keystone/keystone.log /var/log/keystone/*wsgi*.log; do
[ -f "$logfile" ] || continue
log_warn "日志末尾:$logfile"
tail -n 30 "$logfile" || true
done
}
# Apache 启动后,等待 Keystone 实际可以签发令牌(不输出令牌内容)。
log_info "等待 Keystone 管理员认证就绪..."
for attempt in {1..12}; do
# 同时捕获 stdout/stderr:部分客户端错误可能写到 stdout,不能直接丢弃。
if KEYSTONE_AUTH_OUTPUT=$(openstack --os-auth-type password token issue 2>&1); then
unset KEYSTONE_AUTH_OUTPUT
log_info "Keystone 管理员认证成功。"
break
else
KEYSTONE_AUTH_STATUS=$?
fi
log_warn "Keystone 认证尚未就绪(第 $attempt/12 次,退出码 $KEYSTONE_AUTH_STATUS)。"
printf '%s\n' "${KEYSTONE_AUTH_OUTPUT:-客户端未返回错误信息。}" >&2
unset KEYSTONE_AUTH_OUTPUT
if [ "$attempt" -eq 12 ]; then
log_err "Keystone 认证失败;请根据上方客户端错误和下方诊断确定原因。"
diagnose_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-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 "=========================================="