-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdeploy-openstack.sh
More file actions
2478 lines (2126 loc) · 90.7 KB
/
Copy pathdeploy-openstack.sh
File metadata and controls
2478 lines (2126 loc) · 90.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/bin/bash
#
# OpenStack Antelope 双节点一键部署脚本
# 基于 openEuler 24.03 LTS SP2
# 参考: openEuler OpenStack SIG 官方文档
#
set -euo pipefail
# ─── 全局变量 ─────────────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
STATE_FILE="$SCRIPT_DIR/.deploy_state"
LOG_DIR="/var/log/openstack-deploy"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
LOG_FILE="$LOG_DIR/deploy-$TIMESTAMP.log"
BACKUP_DIR="$HOME/openstack-deploy/backup"
NODE_TYPE=""
MGMT_IP=""
CONTROLLER_IP=""
COMPUTE_IP=""
PROVIDER_IFACE=""
COMPUTE_PROVIDER_IFACE=""
COMPUTE_ROOT_PASS=""
PASSWORD_MODE=""
DEPLOY_MODE=""
STATIC_IP_ENABLED="yes"
COMPUTE_STATIC_IP_ENABLED="yes"
DEFAULT_PASS="123456"
CINDER_DISK=""
SWIFT_LOOPBACK=false
# 所有密码变量,默认值 123456
ADMIN_PASS="$DEFAULT_PASS"
RABBIT_PASS="$DEFAULT_PASS"
KEYSTONE_DBPASS="$DEFAULT_PASS"
GLANCE_PASS="$DEFAULT_PASS"
GLANCE_DBPASS="$DEFAULT_PASS"
PLACEMENT_PASS="$DEFAULT_PASS"
PLACEMENT_DBPASS="$DEFAULT_PASS"
NOVA_PASS="$DEFAULT_PASS"
NOVA_DBPASS="$DEFAULT_PASS"
NEUTRON_PASS="$DEFAULT_PASS"
NEUTRON_DBPASS="$DEFAULT_PASS"
CINDER_PASS="$DEFAULT_PASS"
CINDER_DBPASS="$DEFAULT_PASS"
SWIFT_PASS="$DEFAULT_PASS"
METADATA_SECRET="$DEFAULT_PASS"
# 架构检测
ARCH=$(uname -m)
IS_ARM=false
[[ "$ARCH" == "aarch64" ]] && IS_ARM=true
# ─── 颜色输出 ─────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
# ─── 辅助函数 ─────────────────────────────────────────────────
log() { local msg="[$(date '+%H:%M:%S')] $*"; echo -e "$msg" | tee -a "$LOG_FILE"; }
info() { log "${BLUE}[INFO]${NC} $*"; }
warn() { log "${YELLOW}[WARN]${NC} $*"; }
success() { log "${GREEN}[OK]${NC} $*"; }
error() { log "${RED}[ERROR]${NC} $*"; exit 1; }
step() { echo ""; log "${CYAN}════════════════════════════════════════${NC}"; log "${CYAN} $*${NC}"; log "${CYAN}════════════════════════════════════════${NC}"; }
# 运行长时间命令,实时显示输出同时写入日志
run_verbose() {
local desc="$1"; shift
info "$desc ..."
"$@" 2>&1 | tee -a "$LOG_FILE"
local rc=${PIPESTATUS[0]}
if [[ $rc -ne 0 ]]; then
error "$desc 失败 (exit code: $rc)"
fi
success "$desc 完成"
}
# 安静运行,输出只写日志
run_quiet() {
local desc="$1"; shift
info "$desc ..."
if "$@" >> "$LOG_FILE" 2>&1; then
success "$desc 完成"
else
error "$desc 失败"
fi
}
check_root() {
[[ "$(id -u)" -eq 0 ]] || error "请以 root 用户运行此脚本"
}
# 幂等检查: 某阶段是否已完成
is_stage_done() {
local stage="$1"
[[ -f "$STATE_FILE" ]] && grep -q "^COMPLETED=.*\<${stage}\>" "$STATE_FILE" 2>/dev/null
}
# 标记阶段完成
mark_stage() {
local stage="$1"
if grep -q "^COMPLETED=" "$STATE_FILE" 2>/dev/null; then
sed -i "s/^COMPLETED=.*/&,${stage}/" "$STATE_FILE"
else
echo "COMPLETED=${stage}" >> "$STATE_FILE"
fi
success "阶段 [$stage] 完成"
}
# 跳过已完成的阶段
skip_if_done() {
local stage="$1"
if is_stage_done "$stage"; then
step "阶段 [$stage] 已完成,跳过 (--force $stage 强制重跑)"
return 0
fi
return 1
}
# Keystone 幂等: 不存在才创建
svc_create() { openstack service list 2>/dev/null | grep -q "$1" || openstack service create --name "$1" "$2"; }
ep_create() { openstack endpoint list 2>/dev/null | grep -q "$1" || openstack endpoint create --region RegionOne "$1" "$2"; }
# 使用 crudini 设置配置,没有 crudini 则用 openstack-config
ini_set() {
local file="$1" section="$2" key="$3" value="$4"
# 备份文件(只备份一次)
local bak="$BACKUP_DIR/$(basename "$file").orig"
if [[ ! -f "$bak" ]]; then
mkdir -p "$BACKUP_DIR"
cp "$file" "$bak" 2>/dev/null || true
fi
if command -v crudini &>/dev/null; then
crudini --set "$file" "$section" "$key" "$value"
elif command -v openstack-config &>/dev/null; then
openstack-config --set "$file" "$section" "$key" "$value"
else
# fallback: sed
if grep -q "^#*\s*${key}\s*=" "$file" 2>/dev/null; then
sed -i "s|^#*\s*${key}\s*=.*|${key} = ${value}|" "$file"
else
# 在对应 section 下插入
sed -i "/^\[${section}\]/a ${key} = ${value}" "$file"
fi
fi
}
# 等待服务端口就绪
wait_port() {
local host="$1" port="$2" max="${3:-30}"
local i=0
while ! bash -c "echo >/dev/tcp/$host/$port" 2>/dev/null; do
i=$((i+1))
if [[ $i -ge $max ]]; then
warn "等待 $host:$port 超时"
return 1
fi
sleep 2
done
return 0
}
# 保存状态文件中的变量
save_state_var() {
local key="$1" val="$2"
if grep -q "^${key}=" "$STATE_FILE" 2>/dev/null; then
sed -i "s|^${key}=.*|${key}=${val}|" "$STATE_FILE"
else
echo "${key}=${val}" >> "$STATE_FILE"
fi
}
get_state_var() {
local key="$1"
grep "^${key}=" "$STATE_FILE" 2>/dev/null | cut -d= -f2- || echo ""
}
# 配置 SSH 免密登录到 Controller (Compute 专用)
setup_ssh_to_controller() {
local ctrl_ip="$1" ctrl_pass="$2"
if ! command -v sshpass &>/dev/null; then
dnf install -y sshpass >> "$LOG_FILE" 2>&1 || {
warn "无法安装 sshpass,回退到手动模式"
return 1
}
fi
if [[ ! -f ~/.ssh/id_rsa ]]; then
ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa -N "" -q
fi
sshpass -p "$ctrl_pass" ssh-copy-id -o StrictHostKeyChecking=no "root@$ctrl_ip" >> "$LOG_FILE" 2>&1 && {
success "SSH 免密登录已配置: root@$ctrl_ip"
return 0
} || {
warn "SSH 配置失败,将回退到手动模式"
return 1
}
}
# 生成 Compute 节点 state file 内容 (All-in-One 模式用)
generate_compute_state_file() {
cat <<EOF
NODE_TYPE=compute
MGMT_IP=$COMPUTE_IP
CONTROLLER_IP=$MGMT_IP
PROVIDER_IFACE=$COMPUTE_PROVIDER_IFACE
PASSWORD_MODE=$PASSWORD_MODE
CINDER_DISK=${REMOTE_CINDER_DISK:-}
ADMIN_PASS=$ADMIN_PASS
RABBIT_PASS=$RABBIT_PASS
KEYSTONE_DBPASS=$KEYSTONE_DBPASS
GLANCE_PASS=$GLANCE_PASS
GLANCE_DBPASS=$GLANCE_DBPASS
PLACEMENT_PASS=$PLACEMENT_PASS
PLACEMENT_DBPASS=$PLACEMENT_DBPASS
NOVA_PASS=$NOVA_PASS
NOVA_DBPASS=$NOVA_DBPASS
NEUTRON_PASS=$NEUTRON_PASS
NEUTRON_DBPASS=$NEUTRON_DBPASS
CINDER_PASS=$CINDER_PASS
CINDER_DBPASS=$CINDER_DBPASS
SWIFT_PASS=$SWIFT_PASS
METADATA_SECRET=$METADATA_SECRET
STATIC_IP_ENABLED=${COMPUTE_STATIC_IP_ENABLED:-yes}
EOF
}
# 等待 dnf/yum 锁释放 (避免 dnf 自更新后锁残留导致后续 yum 命令卡住)
wait_dnf() {
local max="${1:-30}" i=0
while [[ $i -lt $max ]]; do
if ! pgrep -x dnf >/dev/null 2>&1 && ! pgrep -x yum >/dev/null 2>&1; then
return 0
fi
sleep 2
i=$((i+1))
done
warn "等待 dnf 锁超时 (${max}s),尝试清理残留锁"
rm -f /var/run/dnf.pid /var/run/yum.pid 2>/dev/null || true
sleep 2
}
# 将管理网卡配置为静态 IP (防止 DHCP 租约过期后 IP 变动)
set_static_ip() {
local iface="$1" ip="$2"
if ! command -v nmcli &>/dev/null; then
warn "nmcli 不可用,跳过静态 IP 配置"
return 1
fi
# 获取该网卡的 NetworkManager 连接名
local conn
conn=$(nmcli -t -f GENERAL.CONNECTION dev show "$iface" 2>/dev/null | grep '^GENERAL.CONNECTION:' | cut -d: -f2)
if [[ -z "$conn" ]]; then
warn "无法获取 $iface 的 NetworkManager 连接,跳过静态 IP 配置"
return 1
fi
# 计算网关 (同网段 .2)
local gateway
gateway=$(echo "$ip" | sed 's/\.[0-9]*$/.2/')
nmcli con mod "$conn" \
ipv4.method manual \
ipv4.addresses "${ip}/24" \
ipv4.gateway "$gateway" \
ipv4.dns "8.8.8.8 114.114.114.114" \
connection.autoconnect yes 2>/dev/null
nmcli con up "$conn" 2>/dev/null || true
success "管理网卡 $iface 已配置为静态 IP: $ip/24,网关: $gateway"
return 0
}
# 检测管理 IP 所在网卡名
detect_mgmt_iface() {
local ip="$1"
ip -4 -br addr show | grep -F "$ip" 2>/dev/null | awk '{print $1}' | head -1
}
# ─── 系统检测 ─────────────────────────────────────────────────
check_os() {
if [[ ! -f /etc/openEuler-release ]]; then
error "此脚本仅支持 openEuler 系统"
fi
local ver
ver=$(cat /etc/openEuler-release)
info "系统: $ver"
if ! echo "$ver" | grep -q "24.03"; then
warn "建议使用 openEuler 24.03 LTS SP2,当前版本: $ver"
warn "继续部署可能存在兼容性问题"
fi
local mem
mem=$(grep MemTotal /proc/meminfo | awk '{print $2}')
if [[ $mem -lt 4000000 ]]; then
warn "内存不足 4GB (当前: $((mem/1024/1024))GB),OpenStack 可能无法正常运行"
if [[ "$NODE_TYPE" == "controller" ]]; then
warn "Controller 节点建议至少 8GB 内存"
fi
fi
local disk_avail
disk_avail=$(df /var --output=avail 2>/dev/null | tail -1)
if [[ ${disk_avail:-0} -lt 20000000 ]]; then
warn "/var 分区可用空间不足 20GB"
fi
}
# 自动检测 ens33 的 IP
detect_ens33_ip() {
local ip
ip=$(ip -4 addr show ens33 2>/dev/null | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | head -1)
if [[ -z "$ip" ]]; then
# fallback: 列所有 IP
ip=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
if [[ -z "$ip" ]]; then
error "无法检测到管理网卡 IP,请手动输入"
fi
fi
echo "$ip"
}
# 自动检测可用的第二块网卡
detect_provider_iface() {
local ifaces
ifaces=$(ip -o link show | awk -F': ' '{print $2}' | grep -v 'lo\|docker\|veth\|br-\|virbr\|tun\|tap' | grep -v '^ens33$')
echo "$ifaces" | head -1
}
# 自动扫描空闲磁盘给 Cinder LVM
detect_free_disk() {
local disks d root_disk
# 找到根文件系统所在的物理磁盘 (支持 LVM/分区/裸盘)
local root_src
root_src=$(findmnt -n -o SOURCE / 2>/dev/null)
# 用 lsblk 向上追溯到物理磁盘 (PKNAME = parent kernel name)
root_disk=$(lsblk -nd -o PKNAME "$root_src" 2>/dev/null | grep -v '^$')
# 如果追溯结果为空(根直接在磁盘分区上), 直接提取磁盘名
if [[ -z "$root_disk" ]]; then
root_disk=$(lsblk -nd -o PKNAME "$(echo "$root_src" | sed 's|/dev/||; s/[0-9]*$//')" 2>/dev/null | grep -v '^$')
[[ -z "$root_disk" ]] && root_disk=$(echo "$root_src" | sed 's|/dev/||; s/[0-9]*$//')
fi
# 找没有分区、未挂载的裸磁盘
disks=$(lsblk -nd -o NAME,TYPE,MOUNTPOINT 2>/dev/null | awk '$2=="disk" {print $1}')
for d in $disks; do
# 排除根文件系统所在磁盘
[[ "$d" == "$root_disk" ]] && continue
# 排除磁盘本身被挂载的情况 (裸盘直接 mount)
findmnt "/dev/$d" &>/dev/null && continue
# 排除有分区被挂载的磁盘 (如 /dev/sdb2 → /boot)
grep -q "/dev/${d}[0-9]" /proc/mounts 2>/dev/null && continue
# 排除有分区作为 LVM PV 的磁盘 (如 /dev/sdb3 → openeuler VG)
pvs --noheadings -o pv_name 2>/dev/null | grep -q "/dev/$d" && continue
echo "/dev/$d"
return 0
done
echo ""
}
# ─── 交互式配置 ─────────────────────────────────────────────────
interactive_config() {
clear
echo -e "${CYAN}"
echo "╔══════════════════════════════════════════════════╗"
echo "║ OpenStack Antelope 一键部署脚本 ║"
echo "║ openEuler 24.03 LTS SP2 ║"
echo "║ 双节点部署 (Controller + Compute) ║"
echo "╚══════════════════════════════════════════════════╝"
echo -e "${NC}"
# 1. 选择节点类型
echo ""
echo -e "${CYAN}请选择部署模式:${NC}"
echo " [1] Controller (控制节点,需手动到 Compute 上再次运行脚本)"
echo " [2] Compute (计算节点)"
echo " [3] All-in-One (Controller 一键远程部署双节点)"
while true; do
read -rp "请输入 [1/2/3]: " node_choice
case "$node_choice" in
1) NODE_TYPE="controller"; break ;;
2) NODE_TYPE="compute"; break ;;
3) NODE_TYPE="controller"; DEPLOY_MODE="allinone"; break ;;
*) echo "请输入 1, 2 或 3" ;;
esac
done
save_state_var "NODE_TYPE" "$NODE_TYPE"
[[ "$DEPLOY_MODE" == "allinone" ]] && save_state_var "DEPLOY_MODE" "allinone"
echo ""
# 2. 管理 IP
local detected_ip
detected_ip=$(detect_ens33_ip)
if [[ -n "$detected_ip" ]]; then
read -rp "检测到管理网卡 (ens33) IP: $detected_ip,是否使用? [Y/n]: " use_detected
if [[ "$use_detected" =~ ^[Nn]$ ]]; then
read -rp "请输入管理 IP: " MGMT_IP
else
MGMT_IP="$detected_ip"
fi
else
read -rp "请输入管理 IP: " MGMT_IP
fi
save_state_var "MGMT_IP" "$MGMT_IP"
echo ""
# 3. Controller IP (Compute 节点需要) 或 Compute IP (Controller 需要)
if [[ "$NODE_TYPE" == "compute" ]]; then
local saved_ctrl_ip
saved_ctrl_ip=$(get_state_var "CONTROLLER_IP")
if [[ -n "$saved_ctrl_ip" ]]; then
read -rp "Controller IP [$saved_ctrl_ip]: " ctrl_ip
CONTROLLER_IP="${ctrl_ip:-$saved_ctrl_ip}"
else
read -rp "请输入 Controller 节点管理 IP: " CONTROLLER_IP
fi
save_state_var "CONTROLLER_IP" "$CONTROLLER_IP"
else
CONTROLLER_IP="$MGMT_IP"
local saved_compute_ip
saved_compute_ip=$(get_state_var "COMPUTE_IP")
if [[ -n "$saved_compute_ip" ]]; then
read -rp "Compute 节点 IP [$saved_compute_ip]: " cmpt_ip
COMPUTE_IP="${cmpt_ip:-$saved_compute_ip}"
elif [[ "$DEPLOY_MODE" == "allinone" ]]; then
# all-in-one 模式: Compute IP 必填
while [[ -z "$COMPUTE_IP" ]]; do
read -rp "请输入 Compute 节点管理 IP (必填): " COMPUTE_IP
done
else
read -rp "请输入 Compute 节点管理 IP (用于 Swift ring, 可稍后设置): " COMPUTE_IP
fi
save_state_var "COMPUTE_IP" "${COMPUTE_IP:-}"
fi
echo ""
# 4. Provider 网卡
local detected_provider
detected_provider=$(detect_provider_iface)
if [[ -n "$detected_provider" ]]; then
read -rp "检测到 Provider 网卡: $detected_provider,是否使用? [Y/n]: " use_provider
if [[ "$use_provider" =~ ^[Nn]$ ]]; then
echo "可用网卡:"
ip -o link show | awk -F': ' '{print " " $2}' | grep -v 'lo\|docker\|veth\|br-\|virbr\|tun\|tap'
read -rp "请输入 Provider 网卡名: " PROVIDER_IFACE
else
PROVIDER_IFACE="$detected_provider"
fi
else
echo "可用网卡:"
ip -o link show | awk -F': ' '{print " " $2}' | grep -v 'lo\|docker\|veth\|br-\|virbr\|tun\|tap'
read -rp "未检测到第二块网卡,请手动输入 Provider 网卡名: " PROVIDER_IFACE
fi
save_state_var "PROVIDER_IFACE" "$PROVIDER_IFACE"
echo ""
# 4b. 静态 IP 配置
echo -e "${CYAN}管理网卡静态 IP 配置:${NC}"
echo " 将当前 DHCP 获取的 IP 转为静态配置,避免重启后 IP 变动"
read -rp " 是否启用? [Y/n]: " static_choice
if [[ "$static_choice" =~ ^[Nn]$ ]]; then
STATIC_IP_ENABLED="no"
info "跳过静态 IP 配置,保持 DHCP"
else
STATIC_IP_ENABLED="yes"
info "将配置静态 IP (网关为同网段 .2)"
fi
save_state_var "STATIC_IP_ENABLED" "$STATIC_IP_ENABLED"
echo ""
# 5. 密码配置
echo -e "${CYAN}密码配置:${NC}"
echo " [1] 所有密码统一为 123456 (默认)"
echo " [2] 所有密码统一自定义"
echo " [3] 逐个服务单独设置 (高级)"
while true; do
read -rp "请选择 [1/2/3]: " pass_choice
case "$pass_choice" in
1)
PASSWORD_MODE="default"
info "使用默认密码 123456"
break
;;
2)
PASSWORD_MODE="unified"
read -rsp "请输入统一密码: " DEFAULT_PASS
echo ""
;;
3)
PASSWORD_MODE="custom"
info "逐项设置密码 (回车使用默认值 123456):"
read -rsp " Admin 密码: " ADMIN_PASS; ADMIN_PASS="${ADMIN_PASS:-$DEFAULT_PASS}"; echo ""
read -rsp " RabbitMQ 密码: " RABBIT_PASS; RABBIT_PASS="${RABBIT_PASS:-$DEFAULT_PASS}"; echo ""
read -rsp " Keystone DB 密码: " KEYSTONE_DBPASS; KEYSTONE_DBPASS="${KEYSTONE_DBPASS:-$DEFAULT_PASS}"; echo ""
read -rsp " Glance 密码: " GLANCE_PASS; GLANCE_PASS="${GLANCE_PASS:-$DEFAULT_PASS}"; echo ""
read -rsp " Glance DB 密码: " GLANCE_DBPASS; GLANCE_DBPASS="${GLANCE_DBPASS:-$DEFAULT_PASS}"; echo ""
read -rsp " Placement 密码: " PLACEMENT_PASS; PLACEMENT_PASS="${PLACEMENT_PASS:-$DEFAULT_PASS}"; echo ""
read -rsp " Placement DB 密码: " PLACEMENT_DBPASS; PLACEMENT_DBPASS="${PLACEMENT_DBPASS:-$DEFAULT_PASS}"; echo ""
read -rsp " Nova 密码: " NOVA_PASS; NOVA_PASS="${NOVA_PASS:-$DEFAULT_PASS}"; echo ""
read -rsp " Nova DB 密码: " NOVA_DBPASS; NOVA_DBPASS="${NOVA_DBPASS:-$DEFAULT_PASS}"; echo ""
read -rsp " Neutron 密码: " NEUTRON_PASS; NEUTRON_PASS="${NEUTRON_PASS:-$DEFAULT_PASS}"; echo ""
read -rsp " Neutron DB 密码: " NEUTRON_DBPASS; NEUTRON_DBPASS="${NEUTRON_DBPASS:-$DEFAULT_PASS}"; echo ""
read -rsp " Cinder 密码: " CINDER_PASS; CINDER_PASS="${CINDER_PASS:-$DEFAULT_PASS}"; echo ""
read -rsp " Cinder DB 密码: " CINDER_DBPASS; CINDER_DBPASS="${CINDER_DBPASS:-$DEFAULT_PASS}"; echo ""
read -rsp " Swift 密码: " SWIFT_PASS; SWIFT_PASS="${SWIFT_PASS:-$DEFAULT_PASS}"; echo ""
read -rsp " Metadata Secret: " METADATA_SECRET; METADATA_SECRET="${METADATA_SECRET:-$DEFAULT_PASS}"; echo ""
break
;;
*)
echo "请输入 1, 2 或 3"
;;
esac
done
# 如果选择了统一模式,设置所有密码
if [[ "$PASSWORD_MODE" == "unified" ]]; then
ADMIN_PASS="$DEFAULT_PASS"
RABBIT_PASS="$DEFAULT_PASS"
KEYSTONE_DBPASS="$DEFAULT_PASS"
GLANCE_PASS="$DEFAULT_PASS"; GLANCE_DBPASS="$DEFAULT_PASS"
PLACEMENT_PASS="$DEFAULT_PASS"; PLACEMENT_DBPASS="$DEFAULT_PASS"
NOVA_PASS="$DEFAULT_PASS"; NOVA_DBPASS="$DEFAULT_PASS"
NEUTRON_PASS="$DEFAULT_PASS"; NEUTRON_DBPASS="$DEFAULT_PASS"
CINDER_PASS="$DEFAULT_PASS"; CINDER_DBPASS="$DEFAULT_PASS"
SWIFT_PASS="$DEFAULT_PASS"
METADATA_SECRET="$DEFAULT_PASS"
fi
save_state_var "PASSWORD_MODE" "$PASSWORD_MODE"
echo ""
# 5b. All-in-One: Compute 节点远程部署配置
if [[ "$DEPLOY_MODE" == "allinone" ]]; then
echo -e "${CYAN}═══ Compute 节点配置 (远程部署) ═══${NC}"
# Compute Provider 网卡
echo ""
echo "可用的 Compute 网卡参考 (本机探测, 请确认 Compute 上对应网卡名):"
ip -o link show | awk -F': ' '{print " " $2}' | grep -v 'lo\|docker\|veth\|br-\|virbr\|tun\|tap'
read -rp "Compute 节点 Provider 网卡名: " COMPUTE_PROVIDER_IFACE
save_state_var "COMPUTE_PROVIDER_IFACE" "$COMPUTE_PROVIDER_IFACE"
# Compute root 密码
while true; do
read -rsp "Compute 节点 root 密码: " COMPUTE_ROOT_PASS
echo ""
if [[ -z "$COMPUTE_ROOT_PASS" ]]; then
error "All-in-One 模式必须提供 Compute root 密码"
fi
# 明文确认
read -rp "是否显示密码以确认输入正确? [y/N]: " show_pass
if [[ "$show_pass" =~ ^[Yy]$ ]]; then
echo -e " 密码明文: ${YELLOW}${COMPUTE_ROOT_PASS}${NC}"
fi
# SSH 验证密码
info "验证 Compute 连接 (root@$COMPUTE_IP)..."
if ! command -v sshpass &>/dev/null; then
dnf install -y sshpass 2>/dev/null || true
fi
if command -v sshpass &>/dev/null; then
if sshpass -p "$COMPUTE_ROOT_PASS" ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 \
"root@$COMPUTE_IP" "echo ok" 2>/dev/null; then
success "Compute 密码验证通过"
break
else
warn "无法连接到 Compute ($COMPUTE_IP),请检查 IP 和密码"
read -rp "是否重新输入密码? [Y/n]: " retry
if [[ "$retry" =~ ^[Nn]$ ]]; then
warn "跳过验证,远程部署阶段可能失败"
break
fi
fi
else
warn "无法安装 sshpass,跳过密码验证"
break
fi
done
info "Compute root 密码已记录 (仅内存, 不落盘)"
# Compute Cinder 磁盘 (可选,留空则部署时自动检测)
read -rp "Compute 节点 Cinder LVM 磁盘 (如 /dev/sdb, 回车=自动检测): " REMOTE_CINDER_DISK
if [[ -n "$REMOTE_CINDER_DISK" ]]; then
info "Compute Cinder 磁盘: $REMOTE_CINDER_DISK"
else
info "未指定,Compute 部署时将自动检测空闲磁盘"
fi
# Compute 静态 IP
read -rp "Compute 节点是否配置静态 IP? [Y/n]: " cmpt_static
if [[ "$cmpt_static" =~ ^[Nn]$ ]]; then
COMPUTE_STATIC_IP_ENABLED="no"
info "Compute 跳过静态 IP 配置,保持 DHCP"
else
COMPUTE_STATIC_IP_ENABLED="yes"
info "Compute 将配置静态 IP (网关为同网段 .2)"
fi
save_state_var "COMPUTE_STATIC_IP_ENABLED" "$COMPUTE_STATIC_IP_ENABLED"
echo ""
fi
# 6. Compute 节点: 扫描 Cinder 磁盘
if [[ "$NODE_TYPE" == "compute" ]]; then
CINDER_DISK=$(detect_free_disk)
if [[ -n "$CINDER_DISK" ]]; then
read -rp "检测到空闲磁盘: $CINDER_DISK,用作 Cinder LVM 后端? [Y/n]: " use_disk
if [[ "$use_disk" =~ ^[Nn]$ ]]; then
read -rp "请输入 Cinder 磁盘路径 (如 /dev/sdc,回车跳过): " CINDER_DISK
fi
else
warn "未检测到空闲磁盘"
info "当前磁盘布局:"
lsblk -o NAME,TYPE,SIZE,MOUNTPOINT 2>/dev/null || true
read -rp "请手动输入 Cinder 磁盘路径 (如 /dev/sdc,回车跳过,部署时自动检测): " CINDER_DISK
fi
save_state_var "CINDER_DISK" "${CINDER_DISK:-}"
echo ""
fi
# 7. Compute: SSH 自动验证 (可选)
if [[ "$NODE_TYPE" == "compute" ]]; then
echo ""
echo -e "${CYAN}自动验证配置 (可选):${NC}"
echo " 输入 Controller root 密码可在部署完成后自动验证节点注册状态"
read -rsp " Controller root 密码 (回车跳过): " CTRL_ROOT_PASS
echo ""
if [[ -n "$CTRL_ROOT_PASS" ]]; then
info "将在部署完成后自动 SSH 到 Controller 执行节点发现与验证"
else
info "跳过自动验证,部署完成后需手动到 Controller 执行 discover_hosts"
fi
fi
# 8. 显示摘要并确认
echo -e "${CYAN}╔══════════════════════════════════════════════════╗${NC}"
echo -e "${CYAN}║ 部署配置摘要 ║${NC}"
echo -e "${CYAN}╠══════════════════════════════════════════════════╣${NC}"
echo -e "${CYAN}║${NC} 部署模式: ${GREEN}$([[ "$DEPLOY_MODE" == "allinone" ]] && echo "All-in-One (双节点)" || echo "$NODE_TYPE")${NC}"
echo -e "${CYAN}║${NC} 管理 IP: ${GREEN}$MGMT_IP${NC}"
[[ "$NODE_TYPE" == "compute" ]] && echo -e "${CYAN}║${NC} Controller: ${GREEN}$CONTROLLER_IP${NC}"
[[ "$DEPLOY_MODE" == "allinone" ]] && echo -e "${CYAN}║${NC} Compute IP: ${GREEN}$COMPUTE_IP${NC}"
echo -e "${CYAN}║${NC} Provider 网卡: ${GREEN}$PROVIDER_IFACE${NC}"
[[ "$DEPLOY_MODE" == "allinone" ]] && echo -e "${CYAN}║${NC} Compute Prov: ${GREEN}$COMPUTE_PROVIDER_IFACE${NC}"
echo -e "${CYAN}║${NC} 密码模式: ${GREEN}$PASSWORD_MODE${NC}"
[[ "$NODE_TYPE" == "compute" && -n "$CINDER_DISK" ]] && echo -e "${CYAN}║${NC} Cinder 磁盘: ${GREEN}$CINDER_DISK${NC}"
[[ "$NODE_TYPE" == "compute" && -z "$CINDER_DISK" ]] && echo -e "${CYAN}║${NC} Cinder 磁盘: ${YELLOW}跳过${NC}"
echo -e "${CYAN}╚══════════════════════════════════════════════════╝${NC}"
read -rp "确认开始部署? [Y/n]: " confirm
if [[ "$confirm" =~ ^[Nn]$ ]]; then
info "用户取消部署"
exit 0
fi
}
# ─── init 阶段 ─────────────────────────────────────────────────
stage_init() {
mkdir -p "$LOG_DIR" "$BACKUP_DIR"
info "日志文件: $LOG_FILE"
info "配置备份: $BACKUP_DIR"
check_os
}
# ╔══════════════════════════════════════════════════════════════╗
# ║ Controller 阶段 ║
# ╚══════════════════════════════════════════════════════════════╝
# ─── C1: 环境准备 ──────────────────────────────────────────────
stage_env_prep_ctrl() {
skip_if_done "env_prep" && return
step "C1: Controller 环境准备"
info "关闭 SELinux..."
setenforce 0 || true
sed -i 's/^SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config
success "SELinux → disabled"
info "关闭 firewalld..."
systemctl stop firewalld 2>/dev/null || true
systemctl disable firewalld 2>/dev/null || true
success "firewalld → disabled"
info "设置主机名..."
hostnamectl set-hostname controller
echo "controller" > /etc/hostname
success "主机名: controller"
info "配置 /etc/hosts..."
grep -q "controller" /etc/hosts 2>/dev/null || \
echo "$MGMT_IP controller" >> /etc/hosts
success "/etc/hosts 已更新"
# 配置静态 IP (防止 DHCP 租约过期后 IP 变动)
if [[ "$STATIC_IP_ENABLED" != "no" ]]; then
local mgmt_iface
mgmt_iface=$(detect_mgmt_iface "$MGMT_IP")
if [[ -n "$mgmt_iface" ]]; then
set_static_ip "$mgmt_iface" "$MGMT_IP" || warn "静态 IP 配置失败,请手动设置"
else
warn "无法检测管理网卡,跳过静态 IP 配置"
fi
else
info "已跳过静态 IP 配置 (用户选择保持 DHCP)"
fi
info "配置 YUM 源..."
if ! rpm -q openstack-release-antelope &>/dev/null; then
warn "yum update 可能需要 10-30 分钟,请耐心等待..."
run_verbose "系统包更新 (yum update)" yum update -y
wait_dnf 60
run_verbose "安装 OpenStack Release 包" yum install -y openstack-release-antelope
fi
# 确保 EPOL 启用
if ! grep -q '^\[EPOL\]' /etc/yum.repos.d/openEuler.repo 2>/dev/null; then
cat >> /etc/yum.repos.d/openEuler.repo << 'EOF'
[EPOL]
name=EPOL
baseurl=http://repo.openeuler.org/openEuler-24.03-LTS-SP2/EPOL/main/$basearch/
enabled=1
gpgcheck=1
gpgkey=http://repo.openeuler.org/openEuler-24.03-LTS-SP2/OS/$basearch/RPM-GPG-KEY-openEuler
EOF
fi
run_verbose "刷新 YUM 缓存" bash -c "yum clean all && yum makecache"
success "YUM 源配置完成"
# 生成 SSH 密钥对(供 Compute ssh-copy-id 及双向互信)
if [[ ! -f ~/.ssh/id_rsa ]]; then
ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa -N "" -q
success "SSH 密钥对已生成"
fi
save_state_var "CTRL_HOSTKEY" "$(cat ~/.ssh/id_rsa.pub 2>/dev/null || echo '')"
mark_stage "env_prep"
}
# ─── C2: 基础服务 (Chrony, MariaDB, RabbitMQ, Memcached) ───────
stage_base_services_ctrl() {
skip_if_done "base_services" && return
step "C2: 基础服务 (Chrony, MariaDB, RabbitMQ, Memcached)"
# Chrony (NTP Server)
info "安装 Chrony..."
dnf install -y chrony >> "$LOG_FILE" 2>&1
sed -i '/^allow /d' /etc/chrony.conf
# 计算网段: 192.168.0.2 → 192.168.0.0/24
local subnet
subnet=$(echo "$MGMT_IP" | sed 's/\.[0-9]*$/.0\/24/')
echo "allow $subnet" >> /etc/chrony.conf
systemctl restart chronyd
systemctl enable chronyd
success "Chrony NTP Server 已配置"
# MariaDB
info "安装 MariaDB..."
run_verbose "安装 MariaDB" dnf install -y mariadb-config mariadb mariadb-server python3-PyMySQL
cat > /etc/my.cnf.d/openstack.cnf << EOF
[mysqld]
bind-address = $MGMT_IP
default-storage-engine = innodb
innodb_file_per_table = on
max_connections = 4096
collation-server = utf8_general_ci
character-set-server = utf8
EOF
systemctl restart mariadb
systemctl enable mariadb
# 如果 mysql_secure_installation 还没执行过
if ! mysql -uroot -p"$DEFAULT_PASS" -e "SELECT 1" &>/dev/null; then
info "初始化 MariaDB..."
# 先尝试无密码连接
if mysql -uroot -e "SELECT 1" &>/dev/null; then
mysql -uroot <<SQL
ALTER USER 'root'@'localhost' IDENTIFIED BY '$DEFAULT_PASS';
DELETE FROM mysql.user WHERE User='';
DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');
DROP DATABASE IF EXISTS test;
DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';
FLUSH PRIVILEGES;
SQL
fi
fi
success "MariaDB 已配置 (root 密码: $DEFAULT_PASS)"
# RabbitMQ
info "安装 RabbitMQ..."
dnf install -y rabbitmq-server >> "$LOG_FILE" 2>&1
systemctl restart rabbitmq-server
systemctl enable rabbitmq-server
# 创建 openstack 用户
if ! rabbitmqctl list_users 2>/dev/null | grep -q openstack; then
rabbitmqctl add_user openstack "$RABBIT_PASS"
rabbitmqctl set_permissions openstack ".*" ".*" ".*"
success "RabbitMQ openstack 用户已创建"
else
rabbitmqctl change_password openstack "$RABBIT_PASS" 2>/dev/null || true
success "RabbitMQ openstack 用户已存在,密码已更新"
fi
# Memcached
info "安装 Memcached..."
dnf install -y memcached python3-memcached >> "$LOG_FILE" 2>&1
sed -i 's/^OPTIONS=.*/OPTIONS="-l 127.0.0.1,::1,controller"/' /etc/sysconfig/memcached
systemctl restart memcached
systemctl enable memcached
success "Memcached 已配置"
mark_stage "base_services"
}
# ─── C3: Keystone ──────────────────────────────────────────────
stage_keystone_ctrl() {
skip_if_done "keystone" && return
step "C3: Keystone 认证服务"
# 创建数据库
info "创建 keystone 数据库..."
mysql -uroot -p"$DEFAULT_PASS" <<SQL
CREATE DATABASE IF NOT EXISTS keystone;
GRANT ALL PRIVILEGES ON keystone.* TO 'keystone'@'localhost' IDENTIFIED BY '$KEYSTONE_DBPASS';
GRANT ALL PRIVILEGES ON keystone.* TO 'keystone'@'%' IDENTIFIED BY '$KEYSTONE_DBPASS';
FLUSH PRIVILEGES;
SQL
success "Keystone 数据库已创建"
# 安装软件包
info "安装 Keystone..."
run_verbose "安装 Keystone" dnf install -y openstack-keystone httpd mod_wsgi python3-openstackclient
# 配置 keystone.conf
cat > /etc/keystone/keystone.conf << EOF
[database]
connection = mysql+pymysql://keystone:$KEYSTONE_DBPASS@controller/keystone
[token]
provider = fernet
EOF
success "keystone.conf 已配置"
# 同步数据库
info "同步数据库..."
su -s /bin/sh -c "keystone-manage db_sync" keystone
# 初始化 Fernet
keystone-manage fernet_setup --keystone-user keystone --keystone-group keystone
keystone-manage credential_setup --keystone-user keystone --keystone-group keystone
# Bootstrap
info "初始化 Keystone..."
keystone-manage bootstrap --bootstrap-password "$ADMIN_PASS" \
--bootstrap-admin-url "http://controller:5000/v3/" \
--bootstrap-internal-url "http://controller:5000/v3/" \
--bootstrap-public-url "http://controller:5000/v3/" \
--bootstrap-region-id RegionOne
# Apache 配置
sed -i '/^ServerName/d' /etc/httpd/conf/httpd.conf 2>/dev/null
echo "ServerName controller" >> /etc/httpd/conf/httpd.conf
ln -sf /usr/share/keystone/wsgi-keystone.conf /etc/httpd/conf.d/
systemctl restart httpd
systemctl enable httpd
# 创建 admin-openrc
cat > ~/admin-openrc << EOF
export OS_PROJECT_DOMAIN_NAME=Default
export OS_USER_DOMAIN_NAME=Default
export OS_PROJECT_NAME=admin
export OS_USERNAME=admin
export OS_PASSWORD=$ADMIN_PASS
export OS_AUTH_URL=http://controller:5000/v3
export OS_IDENTITY_API_VERSION=3
export OS_IMAGE_API_VERSION=2
EOF
source ~/admin-openrc
# 创建 service project 和 demo 资源
info "创建 Keystone 资源..."
openstack domain create --description "An Example Domain" example 2>/dev/null || true
openstack project create --domain default --description "Service Project" service 2>/dev/null || true
openstack project create --domain default --description "Demo Project" myproject 2>/dev/null || true
openstack user create --domain default --password "$ADMIN_PASS" myuser 2>/dev/null || true
openstack role create myrole 2>/dev/null || true
openstack role add --project myproject --user myuser myrole 2>/dev/null || true
# 验证
info "验证 Keystone..."
unset OS_AUTH_URL OS_PASSWORD
openstack --os-auth-url http://controller:5000/v3 \
--os-project-domain-name Default --os-user-domain-name Default \
--os-project-name admin --os-username admin token issue --os-password "$ADMIN_PASS" > /dev/null
source ~/admin-openrc
success "Keystone 验证通过"
mark_stage "keystone"
}
# ─── C4: Glance ────────────────────────────────────────────────
stage_glance_ctrl() {
skip_if_done "glance" && return
step "C4: Glance 镜像服务"
mysql -uroot -p"$DEFAULT_PASS" <<SQL
CREATE DATABASE IF NOT EXISTS glance;
GRANT ALL PRIVILEGES ON glance.* TO 'glance'@'localhost' IDENTIFIED BY '$GLANCE_DBPASS';
GRANT ALL PRIVILEGES ON glance.* TO 'glance'@'%' IDENTIFIED BY '$GLANCE_DBPASS';
FLUSH PRIVILEGES;
SQL
source ~/admin-openrc
openstack user list 2>/dev/null | grep -q glance || openstack user create --domain default --password "$GLANCE_PASS" glance
openstack role add --project service --user glance admin 2>/dev/null || true
openstack service list 2>/dev/null | grep -q glance || openstack service create --name glance --description "OpenStack Image" image
openstack endpoint list 2>/dev/null | grep -q image || {
openstack endpoint create --region RegionOne image public http://controller:9292
openstack endpoint create --region RegionOne image internal http://controller:9292
openstack endpoint create --region RegionOne image admin http://controller:9292
}
run_verbose "安装 Glance" dnf install -y openstack-glance
cat > /etc/glance/glance-api.conf << EOF
[database]
connection = mysql+pymysql://glance:$GLANCE_DBPASS@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_PASS
[paste_deploy]
flavor = keystone
[glance_store]
stores = file,http
default_store = file
filesystem_store_datadir = /var/lib/glance/images/
EOF
su -s /bin/sh -c "glance-manage db_sync" glance
systemctl enable openstack-glance-api.service
systemctl restart openstack-glance-api.service
# 等待 glance-api 就绪
info "等待 Glance API 就绪..."
wait_port controller 9292 30
# 下载并上传测试镜像 (跳过: 可在 Horizon Web 自行上传)
info "cirros 测试镜像下载已跳过 (可在 Horizon Web 界面手动上传)"
openstack image list
success "Glance 部署完成"
mark_stage "glance"
}
# ─── C5: Placement ─────────────────────────────────────────────
stage_placement_ctrl() {
skip_if_done "placement" && return
step "C5: Placement 资源调度"
mysql -uroot -p"$DEFAULT_PASS" <<SQL
CREATE DATABASE IF NOT EXISTS placement;
GRANT ALL PRIVILEGES ON placement.* TO 'placement'@'localhost' IDENTIFIED BY '$PLACEMENT_DBPASS';
GRANT ALL PRIVILEGES ON placement.* TO 'placement'@'%' IDENTIFIED BY '$PLACEMENT_DBPASS';
FLUSH PRIVILEGES;
SQL
source ~/admin-openrc
openstack user list 2>/dev/null | grep -q placement || openstack user create --domain default --password "$PLACEMENT_PASS" placement
openstack role add --project service --user placement admin 2>/dev/null || true
openstack service list 2>/dev/null | grep -q placement || openstack service create --name placement --description "Placement API" placement
openstack endpoint list 2>/dev/null | grep -q placement || {
openstack endpoint create --region RegionOne placement public http://controller:8778
openstack endpoint create --region RegionOne placement internal http://controller:8778
openstack endpoint create --region RegionOne placement admin http://controller:8778
}
run_verbose "安装 Placement API" dnf install -y openstack-placement-api
cat > /etc/placement/placement.conf << EOF
[placement_database]
connection = mysql+pymysql://placement:$PLACEMENT_DBPASS@controller/placement
[api]
auth_strategy = keystone