通過 PXE 自動化安裝 Ubuntu Server

hedzr發表於2022-01-02

摘要: 關於如何構造一臺 PXE 伺服器,以及 Ubuntu autoinstall 功能的實際應用。

注意: 閱讀本文你必須具備一定的 bash 程式設計知識。

附言: 本文是跨年版本,原本打算年前發出的,但是這兩天耽擱了,只好現在了。

前言

在雲環境中,雲服務商提供了主機模板(和伺服器映象)以便加速伺服器節點的開設。這類功能(包括像 Vultr 那樣的或者各種 VPS 提供商那樣的)有幾種不同的架構方法,一般情況下主要是通過 KVM 底層結構,搭配上層的管理模組如 Cobble 之類來組成。

PXE

為了做到從客戶下單就觸發全自動操作,則需要 PXE 機制的介入,使得新節點主機從加電開始就開始如下的流程:

  1. 嘗試尋找 DHCP 伺服器,並從 PXE Server 獲得 DHCP 的 IP 地址,以及額外的 BOOTP 引數
  2. 使用 BOOTP 引數(通常是一個檔名“pxelinux.0”),從 PXE Server 的 TFTP 服務中獲取啟動檔案 pxelinux.0
  3. pxelinux.0 啟動檔案開始一整套 linux 引導序列,包括:

    1. 查詢 grub 資訊並顯示 grub 選單
    2. 在使用者選擇了 grub 選單條目、或者預設條目命中時,載入對應的 vmlinuz 與 initrd 去引導 Linux 的核心
    3. 引導該核心時總是配搭 install 引數,所以將會自動進入到 Linux 標準的安裝介面
    4. 由 autoinstall 所提供的 cloud-config 引數(即 user-data 檔案)在安裝過程中自動提供應答資料,從而令安裝介面能夠自動推進
  4. cloud-init 機制負責解釋 meta-data 資料
  5. cloud-init 機制促使後期指令碼完成服務商所需的其它任務

雲服務商們經由上述機制,就能提供完整的線上開機服務了。

當然這裡面的細節還非常地多,不過那就是填入人命的問題了。

對於其它 OS 來說,pxelinux.0 可以是別的 bootloader,甚至檔名也不必如此。

cloud-init

cloud-init 是一整套的主機節點從零開始的開機機制,由 Canonical 研發,並且是當前主流雲服務商的事實上的開機標準,不同的 OS 均能通過這套機制的對接和裝飾達到無人看管的開機工作。

在 Ubuntu 中,現在是使用所謂的 autoinstall 機制來與 cloud-init 做對接。由於兩者的開發商都是一個人,所以不妨將 autoinstall 看作是 Ubuntu 版本的 cloud-init 具體實現。

早期的 Ubuntu,以及 Debian 系,都是使用 preseed 機制來做無人看管安裝作業系統的任務,但現在已經被 cloud-init 和 autoinstall 所接管。

在 RedHat 系中以前是使用 Kickstart 機制來做無人看管安裝任務,現在在雲上則是通過符合 Openstack 規範的機制經由 cloud-init 來完成。其它作業系統也有類似的方案。

這些內容就大大超出本文的綱要範圍。

正文

範圍

我們就只講解一段在本地模擬相應場景的例項,提供一組基礎指令碼,以達到展示這套基本流程的目的。

這裡不僅僅是準備一臺 PXE 伺服器,也是為了提供一套可重用,易於調整的 devops 運維範例。完全不必使用任何已知的高層包裝器。

在本文中,你會看到我們建設了一臺 pxe-server,然後通過它支援其它新虛擬機器無人值守地全自動完成作業系統安裝和工作環境的配置。

概要

我們的環境是建設在 VMWare 中的,所有 VMs 均使用單一的網路卡掛接為 NAT 方式,NAT 網路被用來模擬雲服務商的網路。

一臺 PXE Server 被執行在 NAT 網段中,提供 DHCP+BOOTP,TFTP 和 WEB 服務,這三者向 NewNode 提供無人看管安裝任務的全部所需材料。

我們在同一網段中建立若干新 VM 主機,並設定啟動 BIOS 型別的 UEFI 方式而非傳統方式。然後直接開機,令其自動查詢 DHCP 獲得 IP 地址,進入安裝序列,完成全部安裝任務後停留在啟動就緒狀態,從而達到了模擬的目的。

準備 PXE 伺服器

PXE 是預啟動執行環境Preboot eXecution EnvironmentPXE,也被稱為預執行環境),它提供了一種使用網路介面(Network Interface)啟動計算機的機制。這種機制讓計算機的啟動可以不依賴本地資料儲存裝置(如硬碟)或本地已安裝的作業系統。

在我們的設想中,LAN 中一臺新的主機節點從裸機上電開始,首先經由 PXE 機制獲得一個啟動環境,然後供給它恰當的安裝系統,以便讓這臺裸機進入自動化安裝流程,最終得到一臺 OS 就緒的可工作執行節點,併入當前的生產環境中成為雲設施的一份子。

所以我們需要在 LAN 中執行一臺 PXE 伺服器來提供 DHCP+BOOTP 服務。其中 DHCP 服務通過 UDP 協議等候裸機的網路卡召喚請求一個新的 IP 地址,BOOTP 附著在 DHCP New IP Requested 報文中回應給裸機網路卡,支援 BOOTP 協議的網路卡就能檢索對應的 BOOTP 檔案並載入它進行首次啟動。PXE 伺服器的 DHCP+BOOP 服務通常回應的啟動檔名為 pxelinux.0,這是可定製的。

img

客戶機網路卡以此檔名向 PXE 伺服器的 TFTP 服務請求該檔案,讀取此檔案到記憶體中特定位置,並將 CPU 執行許可權交給該啟動位置,進入相應的啟動流程。

pxelinux.0 的典型流程:

  • 一般來說,此流程取得 TFTP 伺服器的 /grub 資料夾,獲得 grub.cfg 並向使用者展示一個帶有倒數計時的 GRUB 啟動選單。

對於自動安裝系統來說,這個選單的預設值是指向 WEB 伺服器的特定位置 I,並從該位置拉回安裝映象執行,從而進入到典型的 Linux 系統安裝流程。注意我們會配給一個無人應答檔案,因此 Linux 系統安裝流程會自動執行全部序列,無需人工介入。

說了這麼多,現在我們來看看怎麼具體地準備這臺 PXE 伺服器

主機IP
PXE-server172.16.207.90
NewNode-

基本系統

首先我們安裝基本系統,注意本文僅針對 Ubuntu 20.04 LTS,所以 PXE-server 也使用此係統。

但這倒並不是必需的。實際上你需要一臺伺服器支援 DHCP, TFTP, WEB 服務就可以了。

根據個人愛好,我們安裝 zsh 和外掛來幫助減輕擊鍵壓力。

總的入口程式碼

我們使用一個指令碼 vms-builder 來做整體的 PXE-server 構建。

一些輔助函式的解釋在後繼章節 bash.sh 中單獨介紹(後記:其實並沒有),目前你可以腦補他們:例如 Install_packages 等價於 sudo apt-get install -y,headline 相當於 高亮文字的 echo,fn_name 能夠得到當前 bash/zsh 函式名,等等。

部分需要用到的變數,此時也可能不做介紹,你可以稍後查閱指令碼原始碼。

這個指令碼中的起點,也就是入口程式碼是這樣的:

vms_entry() {
    headline "vms-builder is running"

    local ubuntu_iso_url="https://${ubuntu_mirrors[0]}/ubuntu-releases/${ubuntu_codename}/${ubuntu_iso}"
    local alternate_ubuntu_iso_url=${alternate_ubuntu_iso_url:-$ubuntu_iso_url}

    local tftp_dir=/srv/tftp
    local full_nginx=-full

    v_install   # install software packages: tftp, dhcp, nginx, etc
    v_config    # and configure its
    v_end       #
}

這是本文與其它相同主題文章的不同之處:我們提供一套編制 bash 指令碼的最佳範例,你可以很容易地調整它,也能夠簡便地藉此範本做其它用途。

此外這是一套支援冥等性的系統配置方法,你可以反覆多次執行指令碼而無需擔心弄出莫名其妙的結果。

因此我們會對程式設計方法進行同步的解說。

v_installv_config 是整個 PXE-server 構造的關鍵入口。其意自明。

軟體包安裝的入口

系統中需要如下的軟體包

PackageUsage
tftp-hpaTFTP 服務提供系統安裝檔案如 pxelinux.0, grub 等
isc-dhcp-serverDHCP+BOOTP 服務
Nginx提供 Ubuntu 20.04 安裝映象

PXE 協議結合了 DHCPTFTP。DHCP 用於查詢合適的啟動伺服器,TFTP 用於下載網路啟動程式(NBP)和附加檔案。

由於 Ubuntu 的安裝程式採用 iso 映象方式(此方式對於我們來說最為方便),因此還需要 Web 伺服器提供下載功能。

好,v_install 將會安裝他們:


v_install() {
    echo && headline "$(fn_name)" && line

    v_install_tftp_server
    v_install_dhcp_server
    v_install_web_server
}

v_install_tftp_server() {
    headline "$(fn_name)"
    install_packages tftpd-hpa
}

v_install_dhcp_server() {
    headline "$(fn_name)"
    install_packages isc-dhcp-server
}

v_install_web_server() {
    headline "$(fn_name)"
    install_packages nginx$full_nginx
}

不再贅述了。

配置軟體包的入口

v_config 處理全部配置動作。

v_config() {
    echo && headline "$(fn_name)" && line

    v_config_dirs
    v_download_iso

    v_config_boot
    v_config_grub

    v_config_bash_skel

    v_config_tftp
    v_config_dhcp
    v_config_nginx

    v_config_aif        # autoinstall files
}

我們將要達成的目標是建立這樣的 TFTP 佈局:

image-20220101132339519

此外,還需要配置 DHCP,Web Server 等等。

下面的章節將會依照 v_config 給定的順序依次解說。

v_config_dirs

我們最終要建立一整顆 tftp 資料夾結構,所以這裡首先做出基本結構:

v_config_dirs() {
    $SUDO mkdir -pv $tftp_dir/{autoinstall,bash,boot/live-server,cdrom,grub,iso,priv}
}

SUDO 是一個防禦性措施。它是這麼定義的:

SUDO=sudo
[ "$(id -u)" = "0" ] && SUDO=

因此對於 root 使用者來說它等同於沒有,而對於其它使用者而言它就是 sudo 指令。

v_download_iso

然後是下載 Ubuntu 20.04 live server iso 檔案。

v_download_iso() {
    headline "$(fn_name)"
    local tgt=$tftp_dir/iso/$ubuntu_iso
    [ -f $tgt ] || {
        wget "$alternate_ubuntu_iso_url" -O $tgt
    }

    grep -qE "$tftp_dir/iso/" /etc/fstab || {
        echo "$tftp_dir/iso/$ubuntu_iso on $tftp_dir/cdrom    iso9660     ro,loop    0 0" | $SUDO tee -a /etc/fstab
        $SUDO mount -a && ls -la --color $tftp_dir/cdrom
    }
}

這裡涉及到一系列預定義的變數,它們是這樣的:

ubuntu_codename=focal
ubuntu_version=20.04.3
ubuntu_iso=ubuntu-${ubuntu_version}-live-server-amd64.iso

ubuntu_mirrors=("mirrors.cqu.edu.cn" "mirrors.ustc.edu.cn" "mirrors.tuna.tsinghua.edu.cn" "mirrors.163.com" "mirrors.aliyun.com")

ubuntu_mirrors 是一個 bash 的陣列型變數,但這個列表實際上僅有第一個值才會被我們用到:

# in vms_entry():
  local ubuntu_iso_url="https://${ubuntu_mirrors[0]}/ubuntu-releases/${ubuntu_codename}/${ubuntu_iso}"
    local alternate_ubuntu_iso_url=${alternate_ubuntu_iso_url:-$ubuntu_iso_url}

一開始我們首先測試檔案有否存在,並根據需要下載 iso 檔案。

在 v_download_iso 的末尾,我們通過 grep 校驗 fstab 是不是尚未修改過,然後新增條目進去,目的是將下載的 iso 檔案掛載到 /srv/tftp/cdrom 中。

自動掛載並不耽誤什麼事,但是今後我們就能很便利地提取 iso 中的檔案了。

v_config_boot

在上一節,我們已經掛載了 iso 檔案到 cdrom/ 中
v_config_boot() {
    # boot files
    local tgt=$tftp_dir/boot/live-server
    [ -f $tgt/vmlinuz ] || {
        $SUDO cp $tftp_dir/cdrom/casper/vmlinuz $tgt/
        $SUDO cp $tftp_dir/cdrom/casper/initrd $tgt/
    }
}

簡單不解釋。

v_config_grub

前提:

local tgt=$tftp_dir/grub

這部分程式碼首先下載和準備 pxelinux.0 檔案;

    [ -f $tftp_dir/pxelinux.0 ] || {
        $SUDO wget http://archive.ubuntu.com/ubuntu/dists/${ubuntu_codename}/main/uefi/grub2-amd64/current/grubnetx64.efi.signed -O $tftp_dir/pxelinux.0
    }

然後從 cdrom/ 中複製 grub 的字型檔案:

在前面小節中,我們已經掛載了 iso 檔案到 cdrom/ 中
    [ -f $tgt/font.pf2 ] || $SUDO cp $tftp_dir/cdrom/boot/grub/font.pf2 $tgt/

然後是生成 grub.cfg 檔案:

    [ -f $tgt/grub.cfg ] || {
        cat <<-EOF | $SUDO tee $tgt/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=3

            menuentry "Ubuntu server 20.04 autoinstall" --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/ubuntu-${ubuntu_version}-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 "Install Ubuntu Server [NEVER USED]" {
                set gfxpayload=keep
                linux  /casper/vmlinuz   quiet  ---
                initrd /casper/initrd
            }

            grub_platform
            # END OF grub.cfg
        EOF

你必須檢視原始碼而不是從頁面上覆制貼上這些程式碼,因為 heredoc 的縮排功能需要 tab 製表符來縮排,而頁面上這些字元的原貌可能已經丟失。

有關 heredoc 的高階技巧請參閱: 認識 Here Document

在這個 grub 選單中,url="http://${PXE_IP}:3001/iso/ubuntu-${ubuntu_version}-live-server-amd64.iso" 給出的是安裝光碟的 iso 映象,這是通過 Web Server 服務訪問的,稍後在 v_config_nginx 中我們會將 tftp 資料夾對映為 listable 的頁面結構。

ds="nocloud-net;s=http://${PXE_IP}:3001/autoinstall/" 指定的是 autoinstall 資料夾,目的是通過 autoinstall 規範提供 meta-data 和 user-data 兩個檔案,它們被用於免值守自動安裝。

ubuntu live server 的 iso 檔案大約是 1GB 上下,所以 ramdisk_size=1500000 指定記憶體磁碟大小到大約 1.5GB 以容納該 iso,還要留出一定餘量給安裝程式。所以你的每臺新主機節點至少需要 2GB 記憶體的配置,否則可能無法完成自動安裝過程。

特權狀態與管道輸出

對於 bash 編寫來說,非特權使用者要想通過 heredoc 生成一個檔案,需要如下的慣用法:

cat <<-EOF | sudo tee filename
EOF

然後會有一些變種,例如追加到 filename 中:

cat <<-EOF | sudo tee -a filename
EOF

這個慣用法是為了解決輸出管道符不能 sudo 的問題:

echo "dsjkdjs" > filename

如果 filename 是受特權保護的,則echo管道輸出會報錯,要想解決問題,就需要改用 cat heredoc | sudo tee filename 句法。

由於 bash 支援多行字串,所以當你不想使用 heredoc 時,也可以:

echo "djask
djska
daskl
dajskldjsakl" | sudo tee filename

但它在簡單無變數展開的場景中尚算可用,若是你的文字內容龐大且可能包含複雜的變數展開,又或者有各種單引號雙引號包圍,那麼 cat heredoc 才是正確的道路。

v_config_bash_skel

v_config_bash_skel 的目的是生成最小的後安裝指令碼 boot.sh:

v_config_bash_skel() {
    [ -f $tftp_dir/bash/boot.sh ] || {
        cat <<-"EOF" | $SUDO tee $tftp_dir/bash/boot.sh
            #!/bin/bash
            # -*- mode: bash; c-basic-offset: 2; tab-width: 2; indent-tabs-mode: t-*-
            # vi: set ft=bash noet ci pi sts=0 sw=2 ts=2:
            # st: 
            #

            echo "booted."
            [ -f custom.sh ] && bash custom.sh

        EOF
    }

    #
    $SUDO touch $tftp_dir/priv/gpg.key
    $SUDO touch $tftp_dir/priv/custom.sh
}

boot.sh 將被在 Ubuntu 安裝完成後,首次啟動就緒時被自動執行。

此外我們還建立 0 長度的備用檔案 gpg.keycustom.sh

如果你想自動灌入專用的金鑰,例如當你需要做 devops 分發部署前簽名時,那麼你可以提供一個有效的 gpg.key 檔案,否則保留 0 長度即可。

如果你需要額外的後後處理指令碼的話,可以提供一個有效的 custom.sh 指令碼檔案。

我們也提供一份更完整的 boot.sh,但可能需要在下一次再做介紹了。

v_config_tftp

v_config_tftp() {
    cat /etc/default/tftpd-hpa
}

什麼都不做!

tftp 預設的配置是指向 /srv/tftp 資料夾,我們就用這個,無需額外配置。

v_config_dhcp

主要目的是配置 DHCP 的 IP 池:

v_config_dhcp() {
    local f=/etc/dhcp/dhcpd.conf
    $SUDO sed -i -r "s/option domain-name .+\$/option domain-name \"$LOCAL_DOMAIN\";/" $f
    $SUDO sed -i -r "s/option domain-name-servers .+\$/option domain-name-servers ns1.$LOCAL_DOMAIN, ns2.$LOCAL_DOMAIN;/" $f

    grep -qE "^subnet $DHCP_SUBNET netmask" $f || {
        cat <<-EOF | $SUDO tee -a $f

            # https://kb.isc.org/v1/docs/isc-dhcp-44-manual-pages-dhcpdconf
            subnet $DHCP_SUBNET netmask $DHCP_MASK {
                option routers             $DHCP_DHCP_ROUTER;
                option domain-name-servers 114.114.114.114;
                option subnet-mask         $DHCP_MASK;
                range dynamic-bootp        $DHCP_RANGE;
                default-lease-time         21600;
                max-lease-time             43200;
                next-server                $DHCP_DHCP_SERVER;
                filename "pxelinux.0";
                # filename "grubx64.efi";
            }

        EOF
    }

    $SUDO systemctl restart isc-dhcp-server.service
}

filename "pxelinux.0"; 命名了 BOOTP 檔名。

涉及到的變數主要有這些:

LOCAL_DOMAIN="ops.local"

DHCP_PRE=172.16.207
DHCP_SUBNET=$DHCP_PRE.0
DHCP_MASK=255.255.255.0
DHCP_DHCP_ROUTER=$DHCP_PRE.2   # it should be a router ip in most cases
DHCP_DHCP_SERVER=$DHCP_PRE.90  # IP address of 'pxe-server'
DHCP_RANGE="${DHCP_PRE}.100  ${DHCP_PRE}.220"  # the pool

PXE_IP=$DHCP_DHCP_SERVER
PXE_HOSTNAME="pxe-server" # BIOS name of PXE server, or IP address

由於是在本地的 VMWare 虛擬機器中進行模擬,所以使用了一個小型的網段規劃。

v_config_nginx

簡單地追加 nginx 配置並重啟 nginx:

v_config_nginx() {
    local f=/etc/nginx/sites-available/default
    grep -qE 'listen 3001' $f || {
        cat <<-EOF | $SUDO tee -a $f

            server {
              listen 3001 default_server;
              listen [::]:3001 default_server;
              root $tftp_dir;
              autoindex on;
              autoindex_exact_size on;
              autoindex_localtime on;
              charset utf-8;
              server_name _;
            }

        EOF
        $SUDO systemctl restart nginx.service
    }
}

這個配置指定了 pxe-server:3003 的 web 服務,在 grub.cfg 中被使用。

v_config_aif

v_config_aif 可謂為重頭戲,它構造了 autoinstall 所需的檔案。

按照 Ubuntu autoinstall 規範,meta-data 可以提供 instance_id 等 key:value 對,但也可以什麼都不提供。

至於 user-data 檔案,則是用於對安裝過程進行自動應答。它的內容較多,但並不難理解。其難度大概在於,什麼可以怎樣調整的問題,有時候找不到依據。不過下面以函式的方式提供出來,特定的佔位符都已經準備就緒,因此你基本上能夠很好地按照自己的意願進行調整——只需要去修改 bash 變數值即可。

下面是函式的全景,略有刪減:

v_config_aif() {
    # autoinstall files
    $SUDO touch $tftp_dir/autoinstall/meta-data

    declare -a na
    local network_str="" str="" n=1 i
    na=($(ifconfig -s -a | tail -n +2 | grep -v '^lo' | awk '{print $1}'))
    for i in ${na[@]}; do
        [[ $n -gt 1 ]] && str=", " || str=""
        str="${str}${i}: {dhcp4: yes,dhcp6: yes}"
        network_str="${network_str}${str}"
        let n++
    done

    grep -qE '^#cloud-config' $tftp_dir/autoinstall/user-data || {
        cat <<-EOF | $SUDO tee $tftp_dir/autoinstall/user-data
            #cloud-config
            autoinstall:
              version: 1
              interactive-sections: []

              # https://ubuntu.com/server/docs/install/autoinstall-reference
              # https://ubuntu.com/server/docs/install/autoinstall-schema
              apt:
                primary:
                  - arches: [default]
                    uri: http://${ubuntu_mirrors[0]}/ubuntu

              user-data:
                timezone: $TARGET_TIMEZONE
                # Europe/London
                disable_root: true
                # openssl passwd -6 -salt 1234
                # mkpasswd -m sha-512
                chpasswd:
                  list: |
                    root: ${TARGET_PASSWORD}
                runcmd:
                  - wget -P /root/ http://$PXE_HOSTNAME:3001/bash/boot.sh
                  - wget -P /root/ http://$PXE_HOSTNAME:3001/priv/gpg.key || echo "no gpg key, skipped"
                  - wget -P /root/ http://$PXE_HOSTNAME:3001/priv/custom.sh || echo "no custom.sh, skipped"
                  - bash /root/boot.sh
                  #- sed -ie 's/GRUB_TIMEOUT=.*/GRUB_TIMEOUT=3/' /target/etc/default/grub

              identity:
                hostname: $TARGET_HOSTNAME
                # username: ubuntu
                # password: "\$6\$exDY1mhS4KUYCE/2\$zmn9ToZwTKLhCw.b4/b.ZRTIZM30JZ4QrOQ2aOXJ8yk96xpcCof0kxKwuX1kqLG/ygbJ1f8wxED22bTL4F46P0"
                username: $TARGET_USERNAME
                password: "${TARGET_PASSWORD}"

              keyboard: {layout: 'us', variant: 'us'}
              # keyboard: {layout: 'gb', variant: 'devorak'}
              locale: $TARGET_LOCALE

              ssh:
                allow-pw: no
                install-server: true
                authorized-keys: [$(n=1 && for arg in "${TARGET_SSH_KEYS[@]}"; do
                # arg=\"$arg\"
                [ $n -gt 1 ] && echo -n ", "
                echo -n "\"$arg\""
                let n++
            done)]

              packages: [$(n=1 && for arg in "${TARGET_PKGS[@]}"; do
                # arg=\"$arg\"
                [ $n -gt 1 ] && echo -n ", "
                echo -n "\"$arg\""
                let n++
            done)]

              storage:
                grub:
                  reorder_uefi: false
                swap:
                  size: 0
                config:
                  # https://askubuntu.com/questions/1244293/how-to-autoinstall-config-fill-disk-option-on-ubuntu-20-04-automated-server-in
                  - {ptable: gpt, path: /dev/sda, preserve: false, name: '', grub_device: false, type: disk, id: disk-sda}
                  
                  - {device: disk-sda, size: 536870912, wipe: superblock, flag: boot, number: 1, preserve: false, grub_device: true, type: partition, id: partition-sda1}
                  - {fstype: fat32, volume: partition-sda1, preserve: false, type: format, id: format-2}
                  
                  - {device: disk-sda, size: 1073741824, wipe: superblock, flag: linux, number: 2,
                    preserve: false, grub_device: false, type: partition, id: partition-sda2}
                  - {fstype: ext4, volume: partition-sda2, preserve: false, type: format, id: format-0}
                  
                  - {device: disk-sda, size: -1, flag: linux, number: 3, preserve: false,
                    grub_device: false, type: partition, id: partition-sda3}
                  - name: vg-0
                    devices: [partition-sda3]
                    preserve: false
                    type: lvm_volgroup
                    id: lvm-volgroup-vg-0
                  - {name: lv-root, volgroup: lvm-volgroup-vg-0, size: 100%, preserve: false, type: lvm_partition, id: lvm-partition-lv-root}
                  - {fstype: ext4, volume: lvm-partition-lv-root, preserve: false, type: format, id: format-1}
                  
                  - {device: format-1, path: /, type: mount, id: mount-2}
                  - {device: format-0, path: /boot, type: mount, id: mount-1}
                  - {device: format-2, path: /boot/efi, type: mount, id: mount-3}

        EOF
    }
}

它用到的 bash 變數另有宣告,部分節錄如下:

TARGET_HOSTNAME="${TARGET_HOSTNAME:-ubuntu-server}"
TARGET_USERNAME="${TARGET_USERNAME:-hz}"
TARGET_PASSWORD="${TARGET_PASSWORD:-$_default_passwd}"
TARGET_LOCALE="${TARGET_LOCALE:-en_US.UTF-8}"
TARGET_SSH_KEYS=(
    "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDxjcUOlmgsabCmeYD8MHnsVxueebIocv5AfG3mpmxA3UZu6GZqnp65ipbWL9oGtZK3BY+WytnbTDMYdVQWmYvlvuU6+HbOoQf/3z3rywkerbNQdffm5o9Yv/re6dlMG5kE4j78cXFcR11xAJvJ3vmM9tGSBBu68DR35KWz2iRUV8l7XV6E+XmkPkqJKr3IvrxdhM0KpCZixuz8z9krNue6NdpyELT/mvD5sL9LG4+XtU0ss7xH1jk5nmAQGaJW9IY8CVGy07awf0Du5CEfepmOH5gJbGwpAIIubAzGarefbltXteerB0bhyyC3VX0Q8lIHZ6GhMZSqfD9vBHRnDLIL"
)
TARGET_PKGS=(
    # net-tools
    # lsof
    curl
    wget
    # whois
)
TARGET_TIMEZONE=Asia/Chongqing

_default_passwd 你可以自行生成:

$ mkpasswd -m sha-512

或者乾脆寫作這樣:

_default_passwd="$(mkpasswd -m sha-512 'password')"

TARGET_SSH_KEYS 可以給出一個陣列,自行調整。

TARGET_PKGS 可以調整,但不建議。有了 curl 和 wget 之後,在 boot.sh 中你可以進一步地、更好地進行安裝後處理,而不必在系統安裝過程中去做。因為 Ubuntu 安裝流程的原因,安裝過程中軟體源的映象指定有可能不能完全生效,所以在安裝過程中不宜安裝太多軟體包,留待首次啟動後再進行操作時軟體源就不會有問題了。

我們提供的更完整的 boot.sh 中包含了自動登入控制檯,免密 sudo 等實用功能,此外 TARGET_SSH_KEYS 提供了遠端 SSH 登入的能力,因此 _default_passwd 隨意指定都可以,基本上你沒有親自用到它的可能,所以預設一個超級複雜(但超級難記憶)的密碼有利於伺服器安全。
背景:cloud-init 和 autoinstall

user-data 是 cloud-init 規範的一部分,但 cloud-init 和 autoinstall 是一家生的,在 Ubuntu 語境內可以互換使用。

注意我們採用了 yaml 配置結構。如果你想,還可以使用 user-data script 等等其它格式。

heredoc 中展開陣列

注意利用 bash 變數展開語法,我們編寫了一段嵌入式指令碼,用於展開 TARGET_SSH_KEYS 這個陣列:

              ssh:
                allow-pw: no
                install-server: true
                authorized-keys: [$(n=1 && for arg in "${TARGET_SSH_KEYS[@]}"; do
                # arg=\"$arg\"
                [ $n -gt 1 ] && echo -n ", "
                echo -n "\"$arg\""
                let n++
            done)]

展開後的效果示意如下:

              ssh:
                allow-pw: no
                install-server: true
                authorized-keys: ["ssh-rsa dskldl", "ssh-rsa djskld"]

類似的做法在 packages 部分也有用到。

小結

全部指令碼準備完成,跑它!

不出意外(當然不會有),那麼現在 pxe-server 已經就緒了。就等著你開新機做試驗了。

試驗效果

現在新建主機節點只要處於 pxe-server 網段中,就能通過網路卡的 pxe 搜尋自動完成安裝。

新主機上電並獲得 DHCP+BOOTP 啟動引數後,執行 pxelinux.0 和自動執行 GRUB 選單項時(圖中處於 inittd 和 vmlinuz 執行狀態):

image-20220101205844882

已經進入到無人值守系統安裝流程的狀態如下圖:

image-20220101205558999

我們沒有解決的問題是:

  • 支援多種系統,多種硬體配置
  • 支援雲設施構架的可程式設計管理與維護
  • 運用 meta-data 後設資料集
  • 等等

這些問題不是本文應該完成的內容。

部分內容今後可能會另文輕量探討

編寫後安裝指令碼

我們已經提供了一份所謂的更完整的 boot.sh 後安裝指令碼,它完成了一臺工作節點必須的基本環境準備,這些環境配置是為了運維人員能夠在節點中的工作更快樂,你可以自己編寫以適應你們的網路架構。

關於後安裝指令碼的解說,也許下一次再做討論吧,本文篇幅夠長了。

bash.sh

請參考 bash.sh,這是一個單獨的檔案,它可以被用作手工編寫 bash 指令碼的基本骨架。

運用它的方法最佳的範例恰如本文中的 vms-builder 指令碼。

bash.sh 提供了一組基礎檢測函式,可以幫助你編寫具備通用性的指令碼。vms-builder 還額外提供了包管理操作的簡要包裝,以便能夠跨平臺應用。

本打算介紹一下 bash.sh 本身,以幫助你理解本文中給出的程式碼,但是發現篇幅已經很長了,我又對《萬字長文》們很不以為然,那就算了,今後再說吧,等不及的就自己去看原始碼得了。

Tarball

本文提及的程式碼,例如 vms-builder,以及其它必要的檔案,以及參考用的資料夾結構等等,均可在 repo 中找到,歡迎取用。

後記

應該一提的是,VMWare 提供專門的 Data Source 可以向 cloud-init 提供資料來源服務,從這個角度來說,本文實際上不必那麼麻煩——但那要 VMWare vSphere 這樣的企業級平臺才行。

這種資料來源供給機制,已經為各大雲服務商所支援,所以 cloud-init 是事實上的雲服務基礎設施準備標準。

這大概是 Canonical 少有的造的令所有人喜聞樂見的輪子了吧。

參考

?

相關文章