适用范围:全新安装完成的 CentOS Stream 9。本文命令均以 root 执行。磁盘、LVM、/etc/fstab、SSH 和 Docker 配置会改变系统状态,执行前必须确认主机、目标磁盘、卷组名与容量。
1. 实施原则
- 保留一个可用的 root 控制台或第二个 SSH 会话,修改 SSH 和挂载配置时不要只依赖唯一 SSH 连接。
- 不要把 LVM 卷组(VG)的空间全部分配给逻辑卷(LV);保留一部分空闲空间以便后续扩容。
- CentOS Stream 9 默认的 XFS 支持在线扩容但不支持缩容。
/home、/data的初始大小必须审慎规划。 - 保持 SELinux 为
Enforcing,不要为了 Docker 而关闭 SELinux。
2. 安装后调整现有 LVM
本节针对当前这台服务器的实际布局,不是重新分区:
/dev/sda 200G
├─/dev/sda1 600M /boot/efi
├─/dev/sda2 1G /boot
└─/dev/sda3 198.4G LVM PV
├─cs-root 70G /
├─cs-swap 7.9G swap
└─cs-home 120.5G /home
当前 /home 使用 XFS。XFS 可以扩容但不能缩小,所以不能直接使用 lvreduce。安全做法是备份 /home,注销所有使用者,删除并重建一个较小的 /home,再恢复数据。下文假设新 /home 为 20G,并把释放的空间留给后续 /data。
先确认真实布局:
lsblk -f
pvs
vgs
lvs -a -o vg_name,lv_name,lv_size,lv_attr,devices
df -hT
2.1 备份 /home
确认备份放在 /root,不要放在 /home 内。根卷当前有足够空间时执行:
cd /root
df -hT / /home
cp -a /etc/fstab /etc/fstab.before-home-rebuild
OLD_HOME_UUID=$(blkid -s UUID -o value /dev/cs/home)
tar --numeric-owner --xattrs --acls --selinux \
-czpf /root/home-backup.tar.gz -C /home .
gzip -t /root/home-backup.tar.gz
tar -tzf /root/home-backup.tar.gz >/dev/null
sha256sum /root/home-backup.tar.gz
只有在压缩包可读且校验完成后才继续。备份文件位于 /root,删除 /dev/cs/home 不会删除它。
2.2 强制注销桌面用户
重建 /home 会关闭用户程序。先确认当前会话:
loginctl list-sessions
loginctl user-status mars.zhang
who
强制注销 mars.zhang 的全部桌面和终端会话,未保存的数据会丢失:
loginctl terminate-user mars.zhang
验证该用户不再有会话或进程:
loginctl list-sessions
pgrep -a -u mars.zhang || true
如果仍有残留进程,先尝试正常终止;只有确认可以丢弃该用户未保存状态时才使用 KILL:
pkill -TERM -u mars.zhang
sleep 3
pgrep -a -u mars.zhang || true
pkill -KILL -u mars.zhang
2.3 卸载并重建 /home
不要让 root 的当前目录位于 /home,并检查是否仍有进程占用:
cd /root
fuser -vm /home
确认没有重要进程后卸载。不要使用 umount -l 或 umount -f 掩盖占用问题:
umount /home
findmnt /home
findmnt /home 没有输出后,删除原 120.5G 逻辑卷并创建 20G 新卷:
lvremove /dev/cs/home
lvcreate -L 20G -n home cs
mkfs.xfs -f /dev/cs/home
mkdir -p /home
mount /dev/cs/home /home
2.4 恢复数据并修正开机挂载
NEW_HOME_UUID=$(blkid -s UUID -o value /dev/cs/home)
tar --numeric-owner --xattrs --acls --selinux \
-xzpf /root/home-backup.tar.gz -C /home
restorecon -RF /home
grep -nE '[[:space:]]/home[[:space:]]' /etc/fstab
如果 /etc/fstab 使用旧 UUID,将其替换为新 UUID;如果使用 /dev/mapper/cs-home,通常无需修改:
sed -i "s/$OLD_HOME_UUID/$NEW_HOME_UUID/" /etc/fstab
findmnt --verify
mount -a
最后检查数据、文件系统和释放出来的 VG 空间:
findmnt /home
df -hT /home
ls -la /home
du -sh /home
pvs
vgs
lvs -o vg_name,lv_name,lv_size,lv_attr,devices
完成后,cs 卷组应有约 100G 空闲空间,可按第 7 节创建 /data。LVM 的关系是:磁盘分区 /dev/sda3 是物理卷(PV),PV 属于卷组 cs(VG),从 VG 分配逻辑卷(LV),LV 格式化后挂载到 /、/home、/data 等目录。
3. 创建管理员用户
以下以 mars.zhang 为例:
useradd -m -s /bin/bash -G wheel mars.zhang
passwd mars.zhang
id mars.zhang
getent group wheel
CentOS Stream 默认允许 wheel 组 sudo。通过 visudo 检查,不要直接编辑 sudoers:
visudo
确认有如下未注释规则:
%wheel ALL=(ALL) ALL
在第二个终端验证,成功前不要退出 root:
su - mars.zhang
sudo -v
sudo whoami
exit
4. 配置仓库与常用工具
dnf makecache
dnf check-update || true
dnf install -y epel-release
dnf makecache
dnf install -y lrzsz sysstat iotop iftop htop telnet bc httpd-tools nethogs expect
如果必须使用阿里云 EPEL,可改用:
dnf install -y https://mirrors.aliyun.com/epel/epel-release-latest-9.noarch.rpm
dnf makecache
可选测试 EPEL:
dnf install -y cowsay sl
5. SSH 优化
备份配置后修改。公司若使用 Kerberos/GSSAPI 单点认证,不应关闭 GSSAPI:
cp -a /etc/ssh/sshd_config /etc/ssh/sshd_config.bak.$(date +%F-%H%M%S)
vi /etc/ssh/sshd_config
增加或修改:
UseDNS no
GSSAPIAuthentication no
先检查语法,再重启并从新终端登录验证:
sshd -t
systemctl restart sshd
systemctl status sshd --no-pager
6. Chrony 时间同步
timedatectl status
timedatectl set-timezone Asia/Shanghai
dnf install -y chrony
cp -a /etc/chrony.conf /etc/chrony.conf.bak.$(date +%F-%H%M%S)
vi /etc/chrony.conf
在 chrony.conf 中新增以下行;如有内部 NTP,优先使用内部时间源:
server ntp1.aliyun.com iburst prefer
systemctl enable --now chronyd
chronyc sources -v
chronyc tracking
timedatectl status
7. 创建独立 /data LVM 数据卷
7.1 使用现有 VG 的空闲空间
先确认 VG 名与可用容量。以下示例用卷组 cs 创建 80 GiB 的数据卷;应按实际空间修改 80G:
vgs
lvs
lvcreate -L 80G -n data cs
mkfs.xfs -f /dev/cs/data
mkdir -p /data
mount /dev/cs/data /data
LV 名为 data,对应设备为 /dev/cs/data,device-mapper 中显示为 /dev/mapper/cs-data。不建议把 LV 再命名为 cs-data。
获取 UUID 并设置自动挂载:
blkid /dev/cs/data
cp -a /etc/fstab /etc/fstab.bak.$(date +%F-%H%M%S)
vi /etc/fstab
将实际 UUID 写入一行:
UUID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx /data xfs defaults 0 0
验证挂载与 fstab:
findmnt --verify
umount /data
mount -a
findmnt /data
df -hT /data
vgs
lvs -o vg_name,lv_name,lv_size,lv_attr
7.2 当前 VG 无空闲空间时增加新磁盘
新增磁盘后,先严格确认设备名,不能凭猜测对系统盘执行 pvcreate:
lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINTS
以下仅在确认新盘为 /dev/sdb 时执行:
pvcreate /dev/sdb
vgextend cs /dev/sdb
vgs
然后按 7.1 创建 /dev/cs/data。若扩展的是系统盘而非新增磁盘,必须按实际分区布局先扩展分区和 PV,不能直接套用 /dev/sdb 命令。
8. 通过 Docker 官方仓库安装 Docker
确认 /data 已正常挂载后执行:
findmnt /data
df -hT /data
dnf install -y dnf-plugins-core
dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
如访问官方源较慢,可选用清华镜像。镜像源需由维护团队验证同步与可信性:
sed -i 's+https://download.docker.com+https://mirrors.tuna.tsinghua.edu.cn/docker-ce+' /etc/yum.repos.d/docker-ce.repo
也可改用中科大源,二选一,不与清华源混用:
dnf config-manager --add-repo http://mirrors.ustc.edu.cn/docker-ce/linux/centos/docker-ce.repo
9. 设置 Docker 数据目录 /data/docker
创建目录并写入配置。第三方镜像加速器是供应链依赖,生产环境只保留已审核、稳定可用的地址;敏感镜像建议使用自建可信仓库。
mkdir -p /etc/docker /data/docker
cat > /etc/docker/daemon.json <<'EOF'
{
"registry-mirrors": [
"https://docker.1ms.run",
"https://docker.1panel.live",
"https://docker.m.ixdev.cn",
"https://hub.rat.dev",
"https://docker.xuanyuan.me",
"https://dockerproxy.net",
"https://docker-registry.nmqu.com",
"https://hub.amingg.com",
"https://docker.amingg.com",
"https://docker.hlmirror.com",
"https://hub1.nat.tf",
"https://hub2.nat.tf",
"https://hub3.nat.tf",
"https://docker.m.daocloud.io",
"https://docker.kejilion.pro",
"https://docker.367231.xyz",
"https://hub.1panel.dev",
"https://dockerproxy.cool",
"https://docker.apiba.cn",
"https://proxy.vvvv.ee"
],
"data-root": "/data/docker"
}
EOF
python3 -m json.tool /etc/docker/daemon.json >/dev/null
systemctl enable --now docker
systemctl status docker --no-pager
验证 Docker、Compose 和实际数据目录:
docker version
docker info | grep -E 'Docker Root Dir|Registry Mirrors'
docker run --rm hello-world
docker compose version
du -sh /data/docker
默认只由 root 使用 Docker。将普通用户加入 docker 组近似授予 root 级主机控制能力,除非有明确需求,不执行 usermod -aG docker <user>。
10. 配置 Bash 提示符
对当前用户生效;若需要管理员用户也使用,请以该用户身份执行:
cat >> ~/.bashrc <<'EOF'
export PS1='[\[\e[34;1m\]\u@\[\e[0m\]\[\e[32;1m\]\H\[\e[0m\]\[\e[31;1m\] \w\[\e[0m\]]\$ '
EOF
source ~/.bashrc
11. 最终验收
hostnamectl
timedatectl status
getenforce
systemctl is-active sshd chronyd docker
findmnt /data
df -hT / /home /data
pvs
vgs
lvs
docker info | grep 'Docker Root Dir'
docker compose version
交付记录建议包含主机名、IP、系统版本、PV/VG/LV 名称与大小、/data UUID、Docker 版本、镜像源配置和验证日期;不得记录明文密码。
默认逐步询问是否执行,输入 y 执行该步骤;使用 -y 自动执行全部步骤,包括 LVM 重建和强制注销用户:
bash initialize-centos9.sh
bash initialize-centos9.sh -y
可调整参数:
bash initialize-centos9.sh \
--home-size 20G \
--data-size 80G \
--admin-user mars.zhang \
--docker-mirror official
流程包括管理员创建、/home 备份和重建、/data 创建、EPEL 与工具、SSH、Chrony、Docker /data/docker 配置、提示符与最终验证。管理员密码始终通过 passwd 交互输入,不会出现在脚本参数中。
已通过 Bash 语法检查和 --help 验证,未在任何服务器执行。复制到目标 Linux 服务器后执行 chmod 750 initialize-centos9.sh。
#!/usr/bin/env bash
# CentOS Stream 9 initialization for the documented cs/root/home layout.
# Default mode asks before each step. Use -y only after reviewing this script.
set -Eeuo pipefail
VG=cs
HOME_LV=/dev/cs/home
HOME_MOUNT=/home
HOME_SIZE=20G
DATA_LV_NAME=data
DATA_MOUNT=/data
DATA_SIZE=80G
ADMIN_USER=mars.zhang
TARGET_USER=mars.zhang
DOCKER_MIRROR=tuna
AUTO_YES=0
BACKUP_FILE=
FSTAB_BACKUP=
usage() {
cat <<'EOF'
Usage: initialize-centos9.sh [options]
Options:
-y, --yes Run every selected step without step prompts
--admin-user USER Administrator account, default: mars.zhang
--home-size SIZE Recreated /home size, default: 20G
--data-size SIZE New /data size, default: 80G
--docker-mirror NAME official, tuna, ustc, or none; default: tuna
-h, --help Show help
The LVM step is destructive: it backs up /home, deletes /dev/cs/home,
recreates it, restores data, and creates /dev/cs/data.
The administrator password is always entered interactively.
EOF
}
fail() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }
valid_size() { [[ $1 =~ ^[1-9][0-9]*[GgMm]$ ]] || fail "Invalid size: $1"; }
confirm_step() {
local title=$1
if (( AUTO_YES )); then
printf '\n>>> %s [auto-confirmed]\n' "$title"
return 0
fi
printf '\n>>> %s\n' "$title"
read -r -p "Run this step? [y/N] " answer
[[ $answer =~ ^([yY]|[yY][eE][sS])$ ]]
}
run_step() {
local title=$1 fn=$2
if confirm_step "$title"; then
"$fn"
else
printf 'Skipped: %s\n' "$title"
fi
}
while [[ $# -gt 0 ]]; do
case $1 in
-y|--yes) AUTO_YES=1; shift ;;
--admin-user) ADMIN_USER=${2:?Missing value}; shift 2 ;;
--home-size) HOME_SIZE=${2:?Missing value}; shift 2 ;;
--data-size) DATA_SIZE=${2:?Missing value}; shift 2 ;;
--docker-mirror) DOCKER_MIRROR=${2:?Missing value}; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) fail "Unknown option: $1" ;;
esac
done
valid_size "$HOME_SIZE"
valid_size "$DATA_SIZE"
[[ $DOCKER_MIRROR =~ ^(official|tuna|ustc|none)$ ]] || fail "docker-mirror must be official, tuna, ustc, or none"
[[ $EUID -eq 0 ]] || fail "Run as root."
for command_name in awk blkid df dnf findmnt fuser loginctl lvcreate lvremove lvs mkfs.xfs mount pgrep pvs tar umount vgs; do
command -v "$command_name" >/dev/null || fail "Required command not found: $command_name"
done
step_user() {
if id "$ADMIN_USER" >/dev/null 2>&1; then
printf 'User %s already exists; keeping its password.\n' "$ADMIN_USER"
else
useradd -m -s /bin/bash "$ADMIN_USER"
printf 'Created user %s.\n' "$ADMIN_USER"
passwd "$ADMIN_USER"
fi
usermod -aG wheel "$ADMIN_USER"
id "$ADMIN_USER"
printf 'Verify sudo from a second terminal before closing root.\n'
}
step_lvm() {
[[ -b "$HOME_LV" ]] || fail "$HOME_LV does not exist; this script expects the original 120G /home LV."
[[ $(findmnt -n -o FSTYPE -M "$HOME_MOUNT") == xfs ]] || fail "$HOME_MOUNT must be an XFS mount."
[[ -z $(findmnt -n -M "$DATA_MOUNT" 2>/dev/null || true) ]] || fail "$DATA_MOUNT is already mounted."
[[ ! -e "/dev/$VG/$DATA_LV_NAME" ]] || fail "/dev/$VG/$DATA_LV_NAME already exists."
awk '$1 !~ /^#/ && $2 == "/home" { found=1 } END { exit !found }' /etc/fstab || fail "No active /home entry in /etc/fstab."
awk '$1 !~ /^#/ && $2 == "/data" { found=1 } END { exit found ? 0 : 1 }' /etc/fstab && fail "An active /data entry already exists in /etc/fstab."
[[ -d "$DATA_MOUNT" ]] || mkdir -p "$DATA_MOUNT"
if find "$DATA_MOUNT" -mindepth 1 -maxdepth 1 -print -quit | grep -q .; then
fail "$DATA_MOUNT is not empty."
fi
cd /root
local timestamp old_uuid new_uuid data_uuid home_bytes root_free required
timestamp=$(date +%F-%H%M%S)
BACKUP_FILE="/root/home-backup-${timestamp}.tar.gz"
FSTAB_BACKUP="/etc/fstab.before-home-rebuild.${timestamp}"
old_uuid=$(blkid -s UUID -o value "$HOME_LV")
[[ -n $old_uuid ]] || fail "Could not read old /home UUID."
home_bytes=$(du -sxB1 "$HOME_MOUNT" | awk '{print $1}')
root_free=$(df -B1 --output=avail /root | awk 'NR==2 {print $1}')
required=$((home_bytes + 1024*1024*1024))
(( root_free >= required )) || fail "Insufficient /root free space for backup."
cp -a /etc/fstab "$FSTAB_BACKUP"
printf 'Creating and verifying %s\n' "$BACKUP_FILE"
tar --numeric-owner --xattrs --acls --selinux -czpf "$BACKUP_FILE" -C "$HOME_MOUNT" .
gzip -t "$BACKUP_FILE"
tar -tzf "$BACKUP_FILE" >/dev/null
sha256sum "$BACKUP_FILE" | tee "${BACKUP_FILE}.sha256"
if id "$TARGET_USER" >/dev/null 2>&1; then
if loginctl list-sessions --no-legend | awk -v user="$TARGET_USER" '$3 == user { found=1 } END { exit !found }'; then
loginctl terminate-user "$TARGET_USER" || true
else
printf 'No active login session for %s; no logout was needed.\n' "$TARGET_USER"
fi
if pgrep -u "$TARGET_USER" >/dev/null; then pkill -TERM -u "$TARGET_USER" || true; sleep 3; fi
if pgrep -u "$TARGET_USER" >/dev/null; then pkill -KILL -u "$TARGET_USER" || true; fi
fi
pgrep -u "$TARGET_USER" >/dev/null && fail "Processes for $TARGET_USER remain."
printf 'Processes accessing %s (kernel mount is expected):\n' "$HOME_MOUNT"
fuser -vm "$HOME_MOUNT" || true
if (( ! AUTO_YES )); then
read -r -p "Type REBUILD_HOME and press Enter to delete the old LV: " answer
[[ $answer == REBUILD_HOME ]] || fail "Destructive confirmation not received."
else
printf 'Destructive LVM action auto-confirmed by -y.\n'
fi
umount "$HOME_MOUNT"
findmnt -n -M "$HOME_MOUNT" >/dev/null 2>&1 && fail "$HOME_MOUNT is still mounted."
lvremove -y "$HOME_LV"
lvcreate -L "$HOME_SIZE" -n home "$VG"
mkfs.xfs -f "$HOME_LV"
mount "$HOME_LV" "$HOME_MOUNT"
new_uuid=$(blkid -s UUID -o value "$HOME_LV")
sed -i "s|$old_uuid|$new_uuid|g" /etc/fstab
tar --numeric-owner --xattrs --acls --selinux -xzpf "$BACKUP_FILE" -C "$HOME_MOUNT"
command -v restorecon >/dev/null && restorecon -RF "$HOME_MOUNT" || true
umount "$HOME_MOUNT"
mount "$HOME_MOUNT"
lvcreate -L "$DATA_SIZE" -n "$DATA_LV_NAME" "$VG"
mkfs.xfs -f "/dev/$VG/$DATA_LV_NAME"
data_uuid=$(blkid -s UUID -o value "/dev/$VG/$DATA_LV_NAME")
printf 'UUID=%s %s xfs defaults 0 0\n' "$data_uuid" "$DATA_MOUNT" >> /etc/fstab
mount "$DATA_MOUNT"
findmnt --verify
findmnt -M "$HOME_MOUNT"
findmnt -M "$DATA_MOUNT"
df -hT "$HOME_MOUNT" "$DATA_MOUNT"
vgs "$VG"
lvs -o vg_name,lv_name,lv_size,lv_attr,devices "$VG"
printf 'Backup retained at %s\n' "$BACKUP_FILE"
}
step_tools() {
dnf makecache
dnf install -y epel-release
dnf makecache
dnf install -y lrzsz sysstat iotop iftop htop telnet bc httpd-tools nethogs expect
}
step_ssh() {
cp -a /etc/ssh/sshd_config "/etc/ssh/sshd_config.bak.$(date +%F-%H%M%S)"
grep -qE '^[[:space:]]*UseDNS[[:space:]]+no' /etc/ssh/sshd_config || printf '\nUseDNS no\n' >> /etc/ssh/sshd_config
grep -qE '^[[:space:]]*GSSAPIAuthentication[[:space:]]+no' /etc/ssh/sshd_config || printf 'GSSAPIAuthentication no\n' >> /etc/ssh/sshd_config
sshd -t
systemctl restart sshd
systemctl is-active sshd
}
step_time() {
timedatectl set-timezone Asia/Shanghai
dnf install -y chrony
cp -a /etc/chrony.conf "/etc/chrony.conf.bak.$(date +%F-%H%M%S)"
grep -qE '^[[:space:]]*server[[:space:]]+ntp1\.aliyun\.com([[:space:]]|$)' /etc/chrony.conf || printf '\nserver ntp1.aliyun.com iburst prefer\n' >> /etc/chrony.conf
systemctl enable --now chronyd
chronyc tracking
}
step_docker() {
findmnt -M "$DATA_MOUNT" >/dev/null || fail "$DATA_MOUNT is not mounted."
dnf install -y dnf-plugins-core
local repo_host
case $DOCKER_MIRROR in
official) repo_host=https://download.docker.com ;;
tuna) repo_host=https://mirrors.tuna.tsinghua.edu.cn/docker-ce ;;
ustc) repo_host=http://mirrors.ustc.edu.cn/docker-ce ;;
none) repo_host=https://download.docker.com ;;
esac
shopt -s nullglob
local repo_files=(/etc/yum.repos.d/*docker*.repo)
local had_docker_repo=0
if (( ${#repo_files[@]} > 0 )); then
had_docker_repo=1
fi
if (( ${#repo_files[@]} == 0 )); then
dnf config-manager --add-repo "$repo_host/linux/centos/docker-ce.repo"
repo_files=(/etc/yum.repos.d/*docker*.repo)
fi
# Rewrite every existing Docker repo file so stale official entries cannot win.
for repo_file in "${repo_files[@]}"; do
sed -i \
-e "s#https://download.docker.com#$repo_host#g" \
-e "s#https://mirrors.tuna.tsinghua.edu.cn/docker-ce#$repo_host#g" \
-e "s#http://mirrors.ustc.edu.cn/docker-ce#$repo_host#g" \
"$repo_file"
done
local docker_state_dir=/var/lib/initialize-centos9
local docker_repo_marker=$docker_state_dir/docker-repo-configured
mkdir -p "$docker_state_dir"
if [[ -e $docker_repo_marker || $had_docker_repo -eq 1 ]]; then
printf '检测到 Docker 仓库已配置过,本次刷新 DNF 元数据。\n'
dnf clean metadata
dnf makecache
else
printf '首次配置 Docker 仓库,跳过额外的 clean metadata/makecache。\n'
fi
touch "$docker_repo_marker"
dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
mkdir -p /etc/docker /data/docker
cat > /etc/docker/daemon.json <<'EOF'
{
"registry-mirrors": [
"https://docker.1ms.run",
"https://docker.1panel.live",
"https://docker.m.ixdev.cn",
"https://hub.rat.dev",
"https://docker.xuanyuan.me",
"https://dockerproxy.net",
"https://docker-registry.nmqu.com",
"https://hub.amingg.com",
"https://docker.amingg.com",
"https://docker.hlmirror.com",
"https://hub1.nat.tf",
"https://hub2.nat.tf",
"https://hub3.nat.tf",
"https://docker.m.daocloud.io",
"https://docker.kejilion.pro",
"https://docker.367231.xyz",
"https://hub.1panel.dev",
"https://dockerproxy.cool",
"https://docker.apiba.cn",
"https://proxy.vvvv.ee"
],
"data-root": "/data/docker"
}
EOF
python3 -m json.tool /etc/docker/daemon.json >/dev/null
systemctl enable --now docker
systemctl is-active docker
docker version
docker compose version
}
step_prompt() {
local prompt='export PS1='\''[\[\e[34;1m\]\u@\[\e[0m\]\[\e[32;1m\]\H\[\e[0m\]\[\e[31;1m\] \w\[\e[0m\]]\$ '\'''
for home_dir in /root "/home/$ADMIN_USER"; do
[[ -d $home_dir ]] || continue
grep -Fqx "$prompt" "$home_dir/.bashrc" 2>/dev/null || printf '\n%s\n' "$prompt" >> "$home_dir/.bashrc"
done
}
step_verify() {
printf '\n===== 最终验收 =====\n'
printf '\n[主机信息]\n'
hostnamectl | sed -n 's/^\(Static hostname\|Transient hostname\|Operating System\|Kernel\|Architecture\|Virtualization\):[[:space:]]*/\1: /p'
printf '\n[时间同步]\n'
timedatectl status | sed -n 's/^\(Local time\|Time zone\|System clock synchronized\|NTP service\):[[:space:]]*/\1: /p'
printf '\n[SELinux]\n'
printf 'SELinux 状态: %s\n' "$(getenforce)"
printf '\n[系统服务]\n'
local service state
for service in sshd chronyd docker; do
state=$(systemctl is-active "$service" 2>/dev/null || true)
if [[ $state == active ]]; then
printf '%-10s 正常(active)\n' "$service"
elif systemctl list-unit-files --type=service --no-legend "$service.service" 2>/dev/null | grep -q .; then
printf '%-10s 异常(%s)\n' "$service" "${state:-未运行}"
else
printf '%-10s 未安装\n' "$service"
fi
done
printf '\n[LVM 与文件系统]\n'
findmnt -M /home || printf '/home: 未挂载\n'
findmnt -M /data || printf '/data: 未挂载\n'
df -hT / /home /data
pvs
vgs
lvs
printf '\n[Docker]\n'
if command -v docker >/dev/null 2>&1; then
docker info 2>/dev/null | grep 'Docker Root Dir' || printf 'Docker 已安装,但 daemon 未正常响应。\n'
docker compose version 2>/dev/null || printf 'Docker Compose 插件未正常响应。\n'
else
printf 'Docker 命令未找到:Docker 安装步骤尚未完成。\n'
fi
printf '\n===== 验收结束 =====\n'
}
cat <<'BANNER'
This script changes system state. The LVM step deletes and recreates /dev/cs/home.
Review the script, verify the target host, and keep a root console or second session.
Use -y only when you accept automatic execution of every selected step.
BANNER
run_step "Create or verify the wheel administrator" step_user
run_step "Back up and rebuild /home, then create /data" step_lvm
run_step "Install EPEL and common tools" step_tools
run_step "Optimize SSH configuration" step_ssh
run_step "Configure Asia/Shanghai time and Chrony" step_time
run_step "Install Docker and configure /data/docker" step_docker
run_step "Configure Bash prompt" step_prompt
run_step "Run final validation" step_verify
printf '\nInitialization script finished.\n'