0x00 前言
笔者近日在受尽各类灵车 ARM 硬件(比如某 Radxa Orion O6 和某 XElite DevKit)折磨后,终于不堪重负购入了一台 MacStudio M2 Max 准备用来当作 Homelab 的主力机器,准备在其上运行笔者的一些容器和软件。然而众所周知苹果的设备存储是金子做的,扩容也因为 M4 系列的火爆而价格高涨,于是笔者准备在上面部署一台使用 NAS 上提供的 iSCSI 作为存储方案的无盘虚拟机。
进行了一些调查后,笔者准备使用 UTM 作为 macOS 上的 Hypervisor 管理软件,并且在其上添加一个 iPXE 来进行 iSCSI 的引导工作。
0x01 准备工作
首先我们在 NAS 上创建 iSCSI target。 在 Debian 系统上使用命令
# apt install targetcli-fb
安装 iSCSI target 的管理软件。在其中创建并设置 iSCSI 存储的目标块设备与 iqn 等信息。笔者的设置如下:
设置完成后,启动 iSCSI 服务端。
# systemctl start rtslib-fb-targetctl.service
# systemctl enable rtslib-fb-targetctl.service
这样,NAS 上的 iSCSI 设置就完成了。笔者在一台电脑上使用 iSCSI initiator 测试后能成功识别到共享出来的块设备了。
然后我们在 MacStudio 上安装 UTM,并且将他的命令行工具映射下来。
% sudo ln -sf /Applications/UTM.app/Contents/MacOS/utmctl /usr/local/bin/utmctl
0x02 安装虚拟机到远程块设备
我们在 UTM 中创建一个使用 qemu 为后端的虚拟机,并且挂上 Ubuntu Server 的安装镜像。启动虚拟机后,进入安装的 livecd 环境的 shelll 并挂载上 iSCSI 块设备。
此时回到安装器,安装器已经可以正常识别到硬盘了,完成接下来的安装步骤即可。
0x03 准备虚拟机的 iPXE 环境
iPXE 是一个功能更加丰富的预启动执行环境,可以让我们方便地挂载并引导 iSCSI 块设备。由于 UTM qemu 的目标平台是 ARM64 UEFI,我们需要先编译一个对应平台的 iPXE 文件。
克隆 iPXE 的 git 仓库。
% git clone https://github.com/ipxe/ipxe
进入 ./config/general.h
文件,添加一些需要的功能
#define PING_CMD
#define IPSTAT_CMD
#define REBOOT_CMD
#define POWEROFF
然后进行编译
% make bin-arm64-efi/snponly.efi ARCH=arm64
这个 snponly.efi
就是我们需要的 ARM64 UEFI 下的可执行文件了。
由于笔者并不是很想修改路由器的 DHCP 来让虚拟机自动获取到 EFI 文件,我们可以手动创建一个磁盘镜像格式化为 EFI 环境可以识别的 FAT32 分区,再导入至 UTM 创建的虚拟机中。
% dd if=/dev/zero of=./efi.img bs=1M count=256 status=progree
% cfdisk efi.img
% losetup --partscan --show --find efi.img
% mount /dev/loop0p1 /mnt
% cp ./bin-arm64-efi/snponly.efi /mnt/
% umount /dev/loop0p1 && losetup -d /dev/loop0
制作好磁盘映像并上传至虚拟机后,运行虚拟机并进入 edk2 环境中创建 iPXE 的启动项。
启动虚拟机,在 iPXE 中测试挂载 iSCSI。
可以看到已经成功 sanhook 上了 NAS 端的 iSCSI 块设备。
0x04 iSCSI 安装系统的后续工作
如果在此时直接在 iPXE 中 sanboot,可以发现虽然能够直接运行 iSCSI 硬盘里 EFI 分区的引导文件,但在引导后会提示找不到 Linux 的 rootfs。这是因为我们并没有在内核引导的 cmdline 中添加 iSCSI 挂载信息,所以内核是不会自动识别到 iSCSI 硬盘上的 userland。因此需要修改内核启动参数和 initramfs。
我们可以在虚拟机中重新挂载上 Linux 的安装镜像,手动进入 livecd 的 shell 挂载 iSCSI 硬盘并 chroot 进去。
# iscsiadm -m discovery -t sendtargets -p PORTAL_IP_ADDRESS
# iscsiadm -m node -T TARGET_NAME -p PORTAL_IP_ADDRESS -l
# mount /dev/sda2 /mnt
# mount /dev/sda1 /mnt/boot/efi
# for i in /dev /dev/pts /proc /sys /run; do sudo mount -B $i /target$i; done
# chroot /mnt
此时修改/etc/default/grub.cfg
,在 cmdline 中加入如下参数:
root=UUID=264cdeb1-13dd-4427-8632-d47fb311dffb ro ISCSI_INITIATOR=iqn.2025-05.kisara.cyp2d6:client ISCSI_TARGET_NAME=iqn.2025-05.kisara.cyp2d6:nvme01 ISCSI_TARGET_IP=28.0.50.240 ISCSI_TARGET_PORT=3260 root=UUID=264cdeb1-13dd-4427-8632-d47fb311dffb ip=28.0.50.220::28.0.50.254:255.255.255.0:client:enp0s1:off rw rootwait
将其中的 Userland 所在分区的 UUID,IP 地址,iqn 信息,网卡名称等改为你使用的即可。修改完成后应用新的 grub 配置。
# update-grub
更新 initramfs 设置。
# touch /etc/initramfs-tools/scripts/iscsi.initramfs
# update-initramfs -v -k $(uname -r) -c
修改 /etc/fstab
文件。
# / was on /dev/sda2 during curtin installation /dev/disk/by-uuid/264cdeb1-13dd-4427-8632-d47fb311dffb / ext4 defaults,_netdev,x-systemd.requires=iscsid.service 0 1 # /boot/efi was on /dev/sda1 during curtin installation /dev/disk/by-uuid/8379-5341 /boot/efi vfat defaults,_netdev,x-systemd.requires=iscsid.service 0 1
这样就成功设置好了内核启动后的环境。可以在 iPXE 中手动测试是否可以正常进行引导。
0x05 设置 iPXE 自动引导
我们回到先前创建的 iPXE 磁盘镜像中,添加一个自动执行脚本autoexec.ipxe
。
在其中写入以下内容:
#!ipxe
dhcp
set keep-san 1
set initiator-iqn iqn.2025-05.kisara.cyp2d6:client
sanhook iscsi:28.0.50.240::::iqn.2025-05.kisara.cyp2d6:nvme01
sanboot --filename \EFI\BOOT\BOOTAA64.EFI
将修改后的磁盘镜像重新导入至虚拟机。至此,我们就可以启动虚拟机后自动引导安装在 iSCSI 硬盘里的系统了。