Files

526 lines
17 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
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 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 认证失败,请根据上方客户端错误检查连接、服务配置或管理员凭证。"
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-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 "=========================================="