708 lines
25 KiB
Bash
Executable File
708 lines
25 KiB
Bash
Executable File
#!/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"
|
||
# 只替换本脚本管理的配置块,保留软件包提供的 VirtualHost / WSGI 配置。
|
||
sed -i '/^# BEGIN env3-4 placement access$/,/^# END env3-4 placement access$/d' "$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"
|
||
|
||
# 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 "=========================================="
|