玩轉 iPXE + VMware

簡介

iPXE 提供了 PXE 網路開機的功能, 並提供了比網卡或 BIOS 上的 firmware 更多的功能, 例如能執行 script, 能顯示選單, 能從 HTTP, iSCSI, ... 開機等, 十分的靈活強大.

iPXE 同時也提供了多種運行模式

  • Chainloading: 以現有 PXE 載入 iPXE 後再進行開機的模式, 在不改動機器的情況下, 讓開機過程更靈活
  • 燒入網卡: 據 iPXE 網站說大部份網卡都有 expansion ROM, 可將 iPXE 燒入

不過, 要注意的是, iPXE 網站與實際的代碼情況有不小的出入, 碰到行為與預期不相符時, 請直接看 Makefile.housekeeping 及源碼.

建立 VM & virtual network

首先以 vmware 建立測試環境. 我們需要至少兩個 VM, 一個做 server, 一個做 client.

新增一段 virtual network (vmnet16), 關掉 DHCP, 改由我們的 server 提供

建立 server VM

  1. 加上一張額外的網卡 (例如 ens37, 原先的網卡用做對外用)
  2. 將上面的網卡加入 vmnet16
  3. 安裝好 Debian
  4. 安裝 dnsmasq 做為 DNS & DHCP & TFTP server

dnsmasq 的設定如下

# /dev/dnsmasq.d/ipxe
cat /etc/dnsmasq.d/ipxe.conf 
port=53
interface=ens37
dhcp-range=10.3.2.100,10.3.2.200,255.255.255.0,12h
dhcp-boot=undionly.kpxe
enable-tftp
tftp-root=/srv/tftp

建立 client VM. 網卡加入 vmnet16

在 BIOS 中設定用網卡開機

如果 VMware 警告 VM 的網卡進入了 promiscuous mode 的話, 可修改 /dev/vmnet16 的權限解決這個問題.

$ ls /dev/vmnet* -l
crw------- 1 root root 119,  0  3月 30 09:01 /dev/vmnet0
crw------- 1 root root 119,  1  3月 30 09:01 /dev/vmnet1
crw-rw-rw- 1 root root 119, 16  3月 30 10:27 /dev/vmnet16
crw------- 1 root root 119,  8  3月 30 09:01 /dev/vmnet8
$ sudo chmod a+rw /dev/vmnet16

或是, 自行加上 udev rules 在開機時修改它的 group, 並將執行 VM 的 user 加上那個 group 中即可取得.

VMware 為了安全考量沒有啟動它, 詳細說明請參考官網

Build iPXE

基本可參考 Using iPXE in VMware 這頁, 但過程中有些問題需要解決.

在 server VM 上, download source code

$ git clone git://git.ipxe.org/ipxe.git

使用 v1.0.0 版

$ git checkout v1.0.0

裝上 build dependencies

$ sudo apt-get install build-essential liblzma-dev libiberty-dev isolinux genisoimage
[...]
$ apt-file list isolinux
[...]
isolinux: /usr/lib/ISOLINUX/isolinux.bin
[...]

雖然我們安裝了 genisoimage, 其實 iPXE 調用的命令其實是 mkisofs, 好在的是, 參數基本兼容, 只要改下 script 即可

$ echo 'diff --git a/src/util/geniso b/src/util/geniso
index 7c2f767..4b2ec4a 100755
--- a/src/util/geniso
+++ b/src/util/geniso
@@ -51,5 +51,5 @@ do
        echo "" KERNEL $g
        cp -p $f $dir/$g
 done >> $cfg
-mkisofs -q -l -o $out -c boot.cat -b isolinux.bin -no-emul-boot -boot-load-size 4 -boot-info-table $dir
+genisoimage -q -l -o $out -c boot.cat -b isolinux.bin -no-emul-boot -boot-load-size 4 -boot-info-table $dir
 rm -fr $dir
' | patch -p1

進到 src 開始 build, 過程會有不少 warning, 所以要去掉 -Werror. 並且 isolinux.bin 的路徑與 Makefile 中定義的不同, 都需要指定. 完整的 build options 可參考這頁

$ cd src
$ make \
  NO_WERROR=1 \
  ISOLINUX_BIN=/usr/lib/ISOLINUX/isolinux.bin 

將 build 好的 undionly.kpxe 放到 /src/tftp

$ sudo mkdir -p /srv/tftp
$ sudo cp ipxe/bin/undionly.kpxe /srv/tftp

啟動 dnsmasq

$ sudo systemctl start dnsmasq.service

就可以試啟動 client VM 了.

自動化開機過程

要讓開機過程自動化, 需要寫些 iPXE 的腳本.

可先在 iPXE command line 行實驗先. 在 iPXE 載入後按 ctrl+b 進到 command line

試試網路載入 initramfs 及 kernel image 並開機. 但要先將 images 放到 server VM 的 tftp 目錄下

$ sudo cp /boot/{initrd.img-4.4.0-1-amd64,vmlinuz-4.4.0-1-amd64} /srv/tftp

回到 client VM, 查詢網卡設備名稱及通過 dhcp 指令取得 IP

gPXE> ifstat
net0: 00:0c:29:9d:bf:15 on UNDI (closed)
  [Link:up, TX:0 TXE:0 RX:0 RXE:0]
gPXE> dhcp net0

接著載入 initramfs 及 kernel image

gPXE> initrd initrd.img-4.4.0-1-amd64
gPXE> kernel vmlinuz-4.4.0-1-amd64

開機

gPXE> boot

因為並沒有指定 root device, 所以會進到 initramfs 救援模式中

搞清楚後, 可在 build 時將 script 嵌進 image 中

$ cat <<END >boot.ipxe
#!ipxe

dhcp net0
initrd initrd.img-4.4.0-1-amd64
kernel vmlinuz-4.4.0-1-amd64
boot
END
$ rm bin/undionly.kpxe
$ make NO_WERROR=1 \
       ISOLINUX_BIN=/usr/lib/ISOLINUX/isolinux.bin \
       EMBED=boot.ipxe \
       bin/undionly.kpxe
$ sudo cp bin/undionly.kpxe /srv/tftp/

在此處搞了很久... 嵌入的 script 不被執行. 初步確認了下, 連內容都沒出現在 bin/undionly.kpxe

$ grep 'initrd.img-4.4.0-1-amd64' bin/undionly.kpxe.tmp

看了看 Makefile.housekeeping, 改成這樣看起來是有嵌進去了

$ make NO_WERROR=1 \
       ISOLINUX_BIN=/usr/lib/ISOLINUX/isolinux.bin \
       EMBEDDED_IMAGE=boot.ipxe \
       bin/undionly.kpxe

但... 還是不被執行. 可以 DEBUG 指定打開特定 module 的 log

$ make NO_WERROR=1 \
       ISOLINUX_BIN=/usr/lib/ISOLINUX/isolinux.bin \
       EMBEDDED_IMAGE=boot.ipxe \
       DEBUG=script \
       bin/undionly.kpxe

嗯~ 是 image 的 signature 不正確, 造成載入 image 時驗証無法通過, 代碼在 image/script.c

static int script_load ( struct image *image ) {
        static const char magic[] = "#!gpxe";
        char test[ sizeof ( magic ) - 1 /* NUL */ + 1 /* terminating space */];

        /* Sanity check */
        if ( image->len < sizeof ( test ) ) {
                DBG ( "Too short to be a script\n" );
                return -ENOEXEC;
        }

        /* Check for magic signature */
        copy_from_user ( test, image->data, 0, sizeof ( test ) );
        if ( ( memcmp ( test, magic, ( sizeof ( test ) - 1 ) ) != 0 ) ||
             ! isspace ( test[ sizeof ( test ) - 1 ] ) ) {
                DBG ( "Invalid magic signature\n" );
                return -ENOEXEC;
        }

好吧, 跪了, 開頭 shebang 後是要寫 gpxe 而非 ipxe, 改過後重新載入應該就能自動開機進到 busybox 中了.

延伸 VMware + iPXE

VMware 提供了類似網卡的 expansion ROM 的功能, 可直接修改 .vmx 加載提供的 iPXE image.

先確認 VM 的網卡類型

$ lspci -nnn | grep Ethernet
02:01.0 Ethernet controller [0200]: Intel Corporation 82545EM Gigabit Ethernet Controller (Copper) [8086:100f] (rev 01)

這裡的 8086:100f 就是目標 ROM 的名稱, extension 可能是 .mrom.rom, build 命令如下

make NO_WERROR=1 \
       ISOLINUX_BIN=/usr/lib/ISOLINUX/isolinux.bin \
       EMBEDDED_IMAGE=boot.ipxe \
       bin/8086100f.rom

放到 /usr/lib/vmware/resources/

$ sudo cp bin/8086100f.rom /usr/lib/vmware/resources/

修改 .vmx 即可

ethernet0.opromsize = 262144
e1000bios.filename = "/usr/lib/vmware/resources/8086100f.rom"

可參考官網的說明. 但要注意的是, 說明有點過時, 所以有時候 .rom 不行時, 就換 .mrom, 相反亦然.

準備無盤開機系統

要準備的東西有

  • rootfs: 以 debootstrap 製作的 Debian rootfs
  • stage1 script: 嵌在 undionly.kpxe 中, 用來載入 stage2 script
  • stage2 script: 實際用來載入 initramfs & kernel image, 並指定 kernel command line 啟動系統的 script
  • 更新 dnsmasq 設定: 讓 dnsmasq 能針對 iPXE 載入前提供一種資訊, 載入後提供另一種資訊
  • http server: 改由 http 載入 initramfs & kernel image, 速度會比 tftp 快得多
  • nfs server: 經由網路提供 rootfs 給無盤機用

deboostrap 產生一個含 initramfs + kernel image 的 Debian rootfs, 放在 /srv/nfs/debian

$ sudo mkdir /srv/nfs
$ sudo debootstrap jessie /srv/nfs/debian http://httpredir.debian.org/debian --include=linux-image-amd64

安裝上 nfs server

$ sudo apt-get install nfs-kernel-server

編輯 /etc/exports, 加上

/srv/nfs/debian	10.3.2.0/24(rw,async,no_subtree_check,crossmnt,no_root_squash)

Stage1 script (boot.ipxe) 內容更新如下

#!gpxe
set user-class lava-slave-stage1
echo dhcp user-class = ${lava-slave}

:alloc_ip
echo allocating IP address...
dhcp net0 && goto load_stage2
sleep 1
goto alloc_ip

:load_stage2
set uri http://${next-server}/${net0/mac}/${filename}
echo loading stage2 script from ${uri}...
chain --name stage2 ${uri} || sleep 1
goto load_stage2

然後重 build 出 undionly.kpxe (這裡打開了 script 執行過程的 log)

$ make NO_WERROR=1 \
       ISOLINUX_BIN=/usr/lib/ISOLINUX/isolinux.bin \
       EMBEDDED_IMAGE=boot.ipxe \
       DEBUG=script \
       bin/undionly.kpxe
$ sudo cp bin/undionly.kpxe /srv/tftp

內容大致為, 經由 dhcp 取得 IP 及相關參數, 然後用一個固定樣式的 URI 載入 stage2 script. ${next-server} 代表 tftp server 的 IP. ${filename}/etc/dnsmasq.d/ipxe.conf 中的 dhcp-boot 指定.

更新後的 /etc/dnsmasq.d/ipxe.conf 如下

port=53
interface=ens37
dhcp-range=10.3.2.100,10.3.2.200,255.255.255.0,12h
dhcp-boot=undionly.kpxe
enable-tftp
tftp-root=/srv/tftp

dhcp-match=set:ipxe,175
dhcp-boot=tag:!ipxe,undionly.kpxe
dhcp-boot=stage2.ipxe

dnsmasq 的設定主要的變動為, 在沒收到 iPXE 發來的 option 175 時, 就發送 undionly.kpxe, 否則就將檔名改成 stage2.ipxe.

準備 http server. 安裝 apache (nginx 當然也行), 供 download stage2 script, initramfs, kernel image 用

$ sudo apt-get install apache

Stage1 script 會去 http server 上它自已 MAC address 的目錄下找到 stage2 script 並載入執行, 假設我們的無盤機的網卡 MAC address 為 00:0c:29:9b:bf:15, 建立目錄

$ sudo mkdir /var/www/html/00:0c:29:9b:bf:15

Stage2 script 的內容 (/var/www/html/00:0c:29:9b:bf:15/stage2.ipxe)

#!ipxe
set baseuri http://${next-server}/${net0/mac}

initrd ${baseuri}/initrd
kernel ${baseuri}/vmlinuz root=/dev/nfs nfsroot=${next-server}:/srv/nfs/debian,intr,rsize=32768,wsize=32768 ip=dhcp rw
boot

上面的 script 會載入 initramfs 及 kernel image 後指定 kernel command line 開機, 開機, 應該很直觀.

最後, 再建立 initramfs 及 kernel image 的 symlinks 讓 stage2 script 能下載到檔案

$ sudo ln -s /srv/nfs/debian/boot/initrd.img-4.4.0-1-amd64 /var/www/html/00:0c:29:9b:bf:15/initrd
$ sudo ln -s /srv/nfs/debian/boot/vmlinuz-4.4.0-1-amd64 /var/www/html/00:0c:29:9b:bf:15/vmlinuz

大工告成, 可以試著開機啦~

2016/04/06 更新

經由 felixonmars 的反饋, 使用 head 版本的話, 問題會少很多

$ git describe --tags
v1.0.0-2248-gf8e1678

一是基本上不會有 warning 了, 二是 src/util/geniso 中會自動判斷使用什麼 command 產生 .iso, 三是加回了變量 EMBED 做向前兼容所以要嵌入 script 時就不需要改用 EMBEDDED_IMAGE 了, 最後是 isolinux.bin 的路徑判斷補全了, 所以 build command 可以簡化為

$ make EMBED=boot.ipxe bin/undionly.kpxe

另外, script 的 signature 可兼容 #!ipxe#!gpxe (見 src/image/script.c

名詞 & 檔案類型

  • .rom: 燒在網卡上 expansion ROM 用的 image
  • .mrom: 當 image 太大時, 例如超過 64kB, 很可能網卡或 BIOS 就無法正確開機. 此時可配合 .mrom, 一個約 3kB 大小的 stub 做為 loader. 但也不是所有的網卡都支持 .mrom, 可參考這裡
  • .kpxe: 用做 chainloading 用的 image, 通常叫 undionly.kpxe

參考資料

關於 iPXE 的 build & 使用

要支持 EFI 可參考這頁最下方

什麼是 pxelinux

How to break infinite iPXE chainloading

完整的 iPXE 選單 + 自啟動 script + 自動安裝範例