自动化批量安装系统:PXE 服务器搭建

前言 此前打算重建 OpenStack .

前言

此前打算重建 OpenStack 集群,顺带想把机房的二十几台服务器从 CentOS 重装成 Ubuntu,这么多台机器一个一个插 U 盘接显示屏手动重装显然不太现实,正好借此机会研究一下 PXE,实现自动化批量安装系统。

PXE 介绍

预启动执行环境(Preboot eXecution Environment,PXE)提供了一种使用网络接口启动计算机的机制,让计算机的启动可以不依赖本地硬盘和已安装的操作系统,常用于无盘系统和通过网络安装系统。

PXE 服务器需要包含 DHCP、TFTP 和 HTTP 三个服务,启动过程如下:

  1. 寻找 DHCP 服务器并获得 IP 地址、PXE 服务器的地址,以及启动文件
  2. 从 PXE 服务器的 TFTP 服务器中获取启动文件
  3. 启动文件执行 Linux 引导序列,从 TFTP 服务器获取 GRUB 配置并显示菜单
  4. 在用户选择了菜单条目或者超时选中默认条目后,从 TFTP 服务器载入对应的内核文件 vmlinuz 与根文件系统 initrd.img 引导内核
  5. 根据 GRUB 菜单条目中的参数,从 HTTP 服务器获取自动安装配置和 ISO 系统镜像
  6. 系统进入安装界面并开始安装

PXE 服务器搭建

注:以下操作均以 Ubuntu 22.04 环境为例

需要安装以下软件包:

  • isc-dhcp-server(DHCP 服务器):sudo apt install isc-dhcp-server
  • tftpd-hpa(TFTP 服务器):sudo apt install tftpd-hpa
  • nginx(HTTP 服务器):sudo apt install nginx-full

HTTP 服务器

配置 Nginx,在 /etc/nginx/sites-available 中新建配置文件,然后在 /etc/nginx/sites-enabled 中创建软链接

pxe.conf
server {
  listen 3001 default_server;
  listen [::]:3001 default_server;
  root /srv/tftp;
  autoindex on;
  autoindex_exact_size on;
  autoindex_localtime on;
  charset utf-8;
  server_name _;
}

其中 /srv/tftp 是 TFTP 服务器的默认目录。

TFTP 服务器

Ubuntu

以 Ubuntu 22.04 为例,下载 ISO 镜像 到 /srv/tftp/iso 并挂载:sudo mount jammy-live-server-amd64.iso /media,复制 casper/initrd 和 casper/vmlinuz 到 TFTP 目录下:

mkdir -p /srv/tftp/boot/live-server
cp /media/casper/initrd /srv/tftp/boot/live-server/
cp /media/casper/vmlinuz /srv/tftp/boot/live-server/

Ubuntu 使用 autoinstall 进行自动安装,需要在 TFTP 目录下建立 autoinstall 目录放置配置文件,命名为 user-data,此外还需在同目录下创建一个 meta-data 文件,内容可以空,但必须存在。配置文件的具体格式可参照 Automated Server installer config file reference

关于自动安装配置文件,强烈建议先手动执行一遍安装过程,然后从 /var/log/installer/autoinstall-user-data 复制生成的文件,不同硬件配置下可能会有不同的配置文件,使用 UEFI 和使用 BIOS 引导的配置也有所不同,手动编写容易出错,我会在接下来的两个部分给出示例配置,但仅供参考。

目录下剩余内容因系统引导方式而有所不同,将分别阐述。

UEFI

复制 EFI/boot/grubx64.efi 到目录下:

cp /media/EFI/boot/grubx64.efi /srv/tftp/

在 grub 目录建立 grub.cfg 配置文件:

grub/grub.cfg
if loadfont /boot/grub/font.pf2 ; then
  set gfxmode=auto
  insmod efi_gop
  insmod efi_uga
  insmod gfxterm
  terminal_output gfxterm
fi

set menu_color_normal=white/black
set menu_color_highlight=black/light-gray

set timeout=5

menuentry "Ubuntu Server 22.04.1 Automated Installer" --id=autoinstall {
    echo "Loading Kernel..."
    # make sure to escape the ';' or surround argument in quotes
    linux /boot/live-server/vmlinuz ramdisk_size=1500000 ip=dhcp url="http://${PXE_IP}:3001/iso/jammy-live-server-amd64.iso" autoinstall ds="nocloud-net;s=http://${PXE_IP}:3001/autoinstall/" root=/dev/ram0 cloud-config-url=/dev/null
    echo "Loading Ram Disk..."
    initrd /boot/live-server/initrd
}

menuentry "Ubuntu Server 22.04.1 Interactive Installer" {
    set gfxpayload=keep
    linux /boot/live-server/vmlinuz ramdisk_size=1500000 ip=dhcp url="http://${PXE_IP}:3001/iso/jammy-live-server-amd64.iso" root=/dev/ram0 cloud-config-url=/dev/null
    echo "Loading Ram Disk..."
    initrd /boot/live-server/initrd
}

grub_platform
# END OF grub.cfg

注意:

  • 将 ${PXE_IP} 改为 PXE 服务器的 IP 地址
  • http://${PXE_IP}:3001/iso/jammy-live-server-amd64.iso 是系统镜像所在的位置
  • http://${PXE_IP}:3001/autoinstall/ 是自动安装配置所在的位置,注意需要以 / 结尾
  • initrd 和 vmlinuz 的路径需与此前一致:/boot/live-server/initrd/boot/live-server/vmlinuz
  • ds="nocloud-net;s=http://${PXE_IP}:3001/autoinstall/" 这里的分号如果没有用双引号包围需要转义
  • 坑:不要在行尾写注释,在初始化 SubiquityServer 时会使用 shlex.split 解析命令行参数,网上找的一个例子在第 9 行后面加了一个注释 # Don't forget the slash at the end.,其中有一个单引号,触发了 ValueError: No closing quotation,直接导致安装失败……

自动安装配置文件示例如下:

autoinstall/user-data

配置文件包含了一些基本的配置,如系统语言、时区、主机名、用户名、密码等,注意密码为加密后的密文。此外还包含了网卡和磁盘分区的配置,网卡暂时都使用 DHCP 获取 IP 地址,待安装完成后再调整为静态 IP。

磁盘分区使用了一个简单的方案,为启动分区划分必要的空间后,将剩余空间作为一个 Logical Volume Group,其中包含一个 Logical Volume,挂载在根目录下,文件系统使用 XFS。上述的配置以手动安装后自动生成的文件为基准,将 partition-2 的 size 改为了 -1,表示使用所有剩余空间;将 lvm_partition-0 的 size 注释掉,这样会默认使用 Logical Volume Group 的所有空间。

完成后的目录结构如下:

├── autoinstall
│   ├── meta-data
│   └── user-data
├── boot
│   └── live-server
│       ├── initrd
│       └── vmlinuz
├── grub
│   └── grub.cfg
├── grubx64.efi
└── iso
    └── jammy-live-server-amd64.iso

BIOS

从 pxelinux 包复制 pxelinux.0

sudo apt install pxelinux
cp /usr/lib/PXELINUX/pxelinux.0 /srv/tftp/

从 syslinux-common 包复制 ldlinux.c32libutil.c32 和 menu.c32

sudo apt install syslinux-common
cp /usr/lib/syslinux/modules/bios/ldlinux.c32 /srv/tftp/
cp /usr/lib/syslinux/modules/bios/libutil.c32 /srv/tftp/
cp /usr/lib/syslinux/modules/bios/menu.c32 /srv/tftp/

在目录下建立 pxelinux.cfg 配置文件:

pxelinux.cfg/default
default menu.c32
menu title Ubuntu Server Jammy Installer

label auto
        menu label Ubuntu Server 22.04.1 Automated Installer
        menu default
        kernel /boot/live-server/vmlinuz
        initrd /boot/live-server/initrd
        append ip=dhcp cloud-config-url=/dev/null url=http://${PXE_IP}:3001/iso/jammy-live-server-amd64.iso autoinstall ds=nocloud-net;s=http://${PXE_IP}:3001/autoinstall/

label interactive
        menu label Ubuntu Server 22.04.1 Interactive Installer
        menu default
        kernel /boot/live-server/vmlinuz
        initrd /boot/live-server/initrd
        append ip=dhcp cloud-config-url=/dev/null url=http://${PXE_IP}:3001/iso/jammy-live-server-amd64.iso

prompt 0
timeout 50
ontimeout auto

注意:

  • 将 ${PXE_IP} 改为 PXE 服务器的 IP 地址
  • http://${PXE_IP}:3001/iso/jammy-live-server-amd64.iso 是系统镜像所在的位置
  • http://${PXE_IP}:3001/autoinstall/ 是自动安装配置所在的位置,注意需要以 / 结尾
  • initrd 和 vmlinuz 的路径需与此前一致:/boot/live-server/initrd/boot/live-server/vmlinuz
  • default menu.c32 是此前 menu.c32 所在的位置
  • timeout 的单位是 1/10 秒timeout 50 意味着超时时间为 5 秒

自动安装配置文件示例如下:

autoinstall/user-data

同样是在生成的配置文件基础上对 Partition 和 Logical Volume Group 的大小进行了调整,可以看到配置文件的内容大致与 UEFI 相同,但在分区时并没有划分 /boot/efi,因此并不能通用。

完成后的目录结构如下:

├── autoinstall
│   ├── meta-data
│   └── user-data
├── boot
│   └── live-server
│       ├── initrd
│       └── vmlinuz
├── iso
│   └── jammy-live-server-amd64.iso
├── ldlinux.c32
├── libutil.c32
├── menu.c32
├── pxelinux.0
└── pxelinux.cfg
    └── default

CentOS

以 CentOS 7 为例,下载 ISO 镜像 并挂载到 /srv/tftp/iso,从中提取 isolinux 目录下的 vmlinuz 和 initrd.img 文件,放置到 TFTP 目录下。

mkdir -p /srv/tftp/iso
mount CentOS-7-x86_64-Everything-2207-02.iso /srv/tftp/iso
cp /srv/tftp/iso/isolinux/vmlinuz /srv/tftp
cp /srv/tftp/iso/isolinux/initrd.img /srv/tftp

pxelinux.0ldlinux.c32libutil.c32 和 menu.c32 的获取方法与 Ubuntu 相同。

CentOS 使用 Kickstart 自动化安装过程,创建 centos7.cfg 文件,内容如下:

centos7.cfg
#platform=x86, AMD64, or Intel EM64T
#version=DEVEL
firewall --disabled
# Install OS instead of upgrade
install
# Keyboard layouts
keyboard 'us'
# Root password: 123456
rootpw --iscrypted $6$E0qvVJCE2JwWSgZQ$PoVqc3oJ0xTFi70PrR6kkXJFWnooC5Z3Zh/KFDMJp5QAUEGfNhGgAUNQ.1KphCh.UhUGi.MFX5akvGDDR1lan0
# Use network installation
url --url="http://${PXE_IP}:3001/iso/"
# System language
lang en_US
# System authorization information
auth  --useshadow  --passalgo=sha512
# Use graphical install
graphical
# SELinux configuration
selinux --disabled
# Do not configure the X Window System
skipx
# Firewall configuration
firewall --disabled
# Network information
network  --bootproto=dhcp --device=em1
# Reboot after installation
reboot
# System timezone
timezone Asia/Shanghai
# System bootloader configuration
bootloader --location=mbr
# Partition clearing information
clearpart --all --initlabel
# Disk partitioning information
autopart

%packages
@core
%end

注意:

  • 将 ${PXE_IP} 改为 PXE 服务器的 IP 地址
  • inst.repo=http://${PXE_IP}:3001/iso/ 是镜像挂载后的位置
  • root 密码为密文,可使用 Python 生成:
    python -c 'import crypt,getpass;pw=getpass.getpass();print(crypt.crypt(pw) if (pw==getpass.getpass("Confirm: ")) else exit())'

不同于 Ubuntu,CentOS 提供了自动分区的选项,省去了配置磁盘分区的烦恼,但默认会将 /home 作为最大的分区,需要视情况调整。

修改 pxelinux.cfg/default

pxelinux.cfg/default
default menu.c32
prompt 0
timeout 30
MENU TITLE PXE Menu
LABEL centos7_x64
MENU LABEL CentOS 7_X64
KERNEL /vmlinuz
APPEND initrd=/initrd.img inst.repo=http://${PXE_IP}:3001/iso/ ks=http://${PXE_IP}:3001/centos7.cfg

注意:

  • 将 ${PXE_IP} 改为 PXE 服务器的 IP 地址
  • KERNEL /vmlinuz 和 initrd=/initrd.img 与此前复制的两个文件对应,均在 TFTP 根目录下
  • inst.repo=http://${PXE_IP}:3001/iso/ 是镜像挂载后的位置
  • ks=http://${PXE_IP}:3001/centos7.cfg 是 Kickstart 自动安装配置文件的位置

相比 Ubuntu 的艰难探索,安装 CentOS 时直接采用了前人的配置,再搭配自动分区功能,省去了不少时间。UEFI 系统的安装暂未尝试,但应与 Ubuntu 类似,不再赘述。

完成后的目录结构如下:

├── centos7.cfg
├── initrd.img
├── iso
├── ldlinux.c32
├── libutil.c32
├── menu.c32
├── pxelinux.0
├── pxelinux.cfg
│   └── default
└── vmlinuz

DHCP 服务器

搭建 PXE 服务器的最后一步是配置 DHCP 服务器,编辑 /etc/dhcp/dhcpd.conf 文件,添加以下内容:

dhcpd.conf
allow booting;
allow bootp;
option arch code 93 = unsigned integer 16;

subnet 192.168.2.0 netmask 255.255.255.0 {
    option routers             192.168.2.1;
    option domain-name-servers 8.8.8.8;
    option subnet-mask         255.255.255.0;
    range dynamic-bootp        192.168.2.200  192.168.2.255;
    default-lease-time         21600;
    max-lease-time             43200;
    next-server                192.168.2.100;
    if option arch = 00:07 {
      filename "grubx64.efi";
    } else {
      filename "pxelinux.0";
    }
}

其中,subnet 和 netmask 指定了 DHCP 分配 IP 的网段,注意不要与现有网段冲突,next-server 是 PXE 服务器的 IP 地址。if option arch = 00:07 用于判断客户端使用的是 UEFI 还是 BIOS 引导,并返回对应的启动文件,使用 UEFI 的机器在 DHCP 服务器上的 vendor-class-identifer 类似于 PXEClient:Arch:00007:UNDI:003016,使用 BIOS 的机器则类似于 PXEClient:Arch:00000:UNDI:002001

系统安装与故障排查

PXE 服务器搭建完毕后便可以开始安装系统了,但在安装过程中仍然可能会遇到一些问题,因此建议先拿一台机器进行测试,之后再批量安装。

将机器的下一次引导方式设置为 PXE 启动,如果是从未安装过系统的机器,PXE 启动应该已经是第一启动项,但这里我使用的是已经安装过系统的机器,因此需要手动调整启动顺序,可以借助 Intel 的 IDRAC 远程批量操控:

pxe-reboot.sh
#!/bin/bash

password="" # IDRAC 密码

for ((i=100;i<=120;i++))
do
    echo "Rebooting node $i..."
    # 192.168.0.1/24 为 IDRAC 网卡的网段,需要根据实际情况修改
    sshpass -p $password ssh -o StrictHostKeyChecking=no root@192.168.0.$i "racadm config -g cfgServerInfo -o cfgServerBootOnce 1"
    sshpass -p $password ssh -o StrictHostKeyChecking=no root@192.168.0.$i "racadm config -g cfgServerInfo -o cfgServerFirstBootDevice PXE"
    sshpass -p $password ssh -o StrictHostKeyChecking=no root@192.168.0.$i "racadm serveraction powercycle"
done

运行完脚本后之后机器就会重启并进入 PXE 启动,如果你的 IDRAC 有企业版许可证,可以通过 VNC 虚拟控制台查看安装过程,还可使用键盘远程操控以及重启。

不得不说这 IDRAC 虚拟控制台真是个好东西,免去了在机房里吸灰和受噪声污染的劳苦,唯一的不足就是需要购买许可证,如果没有的话可以试试 这里 的延长试用版,最多可试用 240 天(如果之前已经导入过试用版证书则无法使用),再者可以去某宝上买正式版永久授权,否则恐怕只能钻进机房接显示屏了(

进入 PXE 启动后你仍然可能会遇到各种问题,需要根据显示屏上的错误信息进行排查,基本思路如下:

  1. 是否成功从 DHCP 服务器获取了 IP 地址?
    • DHCP 服务器是否正常工作?
    • 防火墙端口是否放行?
    • DHCP 服务器和 PXE 网卡是否在同一网络内?
  2. 是否从 TFTP 服务器获取了启动文件?
    • TFTP 服务器的地址是否正确?(DHCP 服务器配置里的 next-server
    • TFTP 服务器是否正常工作?
    • 防火墙端口(69 号)是否放行?
    • 进行 PXE 启动的服务器能否连接到 TFTP 服务器?
    • 启动文件名(DHCP 配置里的 filename)是否正确?
    • TFTP 服务器的目录及文件的权限设置是否正确?
  3. 是否进入了启动菜单?
    • DHCP 服务器配置的启动文件是否正确?注意 BIOS 应使用 pxelinux.0,UEFI 应使用 grubx64.efi
    • 是否从 TFTP 服务器获取了对应文件?BIOS 为 pxelinux.cfg/default,UEFI 为 grub/grub.cfg
    • 配置文件的格式是否正确?
  4. 是否成功获取了 vmlinuz 和 initrd.img
    • pxelinux.cfg/default 和 grub/grub.cfg 中的文件路径是否正确?
    • 是否能正常连接到 TFTP 服务器?
    • TFTP 服务器的目录及文件的权限设置是否正确?
  5. 进入内核后,是否成功下载了系统镜像文件?
    • pxelinux.cfg/default 和 grub/grub.cfg 中的镜像地址是否正确?
    • 是否能正常连接到 HTTP 服务器?
    • HTTP 服务器的目录及文件的权限设置是否正确?
  6. 是否开始自动安装?
    • pxelinux.cfg/default 和 grub/grub.cfg 中自动安装配置文件的地址是否正确?
    • 内核启动的参数是否正确?
    • 配置文件的格式是否正确?
    • 是否能正常连接到 HTTP 服务器?
    • HTTP 服务器的目录及文件的权限设置是否正确?
  7. 安装是否成功?
    • 配置文件的格式是否正确?
    • 配置是否正确?如磁盘分区时没有配置 bootloader 分区
    • 安装失败时可按 Ctrl+Alt+F2 进入 Shell 查看日志
      • Curtin 磁盘分区日志:/var/log/curtin
      • cloud-init 日志:/var/log/cloud-init.log
      • Subiquity 日志:/var/log/subiquity-server.debug.log

安装完毕后,即可通过 SSH 使用 root 用户和安装配置中指定的密码登录系统。那么如何找到机器的 IP 地址呢?最简单粗暴的办法就是把 DHCP 地址段中的 IP 全部扫描一遍,但更合理的办法是查看 DHCP 的分配记录,你可以使用 systemctl status isc-dhcp-server 查看 DHCP 服务器的日志,也可以直接在 /var/lib/dhcp/dhcpd.leases 中找到全部记录。

/var/lib/dhcp/dhcpd.leases
lease 192.168.2.209 {
  starts 4 2022/11/22 08:00:00;
  ends 4 2022/11/22 14:00:00;
  tstp 4 2022/11/22 14:00:00;
  cltt 4 2022/11/22 08:00:00;
  binding state free;
  hardware ethernet 12:34:de:ad:be:ef;
  set vendor-class-identifier = "PXEClient:Arch:00000:UNDI:002001";
}

例如以上的记录就表示 MAC 地址为 12:34:de:ad:be:ef 的机器在 2022/11/22 08:00:00 申请 IP,分配到的 IP 为 192.168.2.209,且使用的是 BIOS 引导。

最后,如果一切顺利的话,你就能使用 root 用户和此前配置的密码登录系统了。

遗留问题

静态 IP 与主机名分配

由于各台机器 PXE 启动的时间不同,因此从 DHCP 分配的 IP 地址与机器编号不匹配,主机名也没有进行区分,有以下几种解决方法:

  • 配置 DHCP 服务器将 IP 与 MAC 地址绑定
  • 利用 cloud-init,根据机架号等硬件信息让每台机器获取到不同的配置文件
  • 安装系统后再修改 IP 地址和主机名,使用 MAC 地址等方式匹配到机器编号,Ubuntu 的网络配置可以借助 Netplan,CentOS 可直接修改 /etc/sysconfig/network-scripts 下的配置文件,然后重启网卡

UEFI 启动顺序

设置 PXE 启动是设置的是下次引导方式,理论上在安装完系统重启后会恢复为从硬盘启动,但使用 UEFI 引导的机器在安装完 Ubuntu 系统重启后,PXE 启动仍然是第一启动项,而使用 BIOS 引导或安装 Centos 系统则不会出现此问题。

此问题的原因暂未查明,一个 workaround 是安装完系统后不要重启而是关机,待所有机器的系统都安装完毕并关机后,将 DHCP 服务器关掉,或是让 DHCP 服务器返回 BIOS 的 pxelinx.0 而不是 UEFI 的 grubx64.efi ,这样 PXE 启动时就会失败(笑),并回退到硬盘启动,最后将启动顺序批量改正即可。(注意在关机后调整启动顺序是无效的,而且此时的启动顺序仍然是正常的硬盘启动,似乎只有在进入引导后的某一时刻才会转为 PXE 启动)

参考资料