Ubuntu 20.04 NFS PXE autoinstall automation


  • Here is a script to setup PXE boot for Ubuntu 20.04 server iso which can be preseed to automate server/desktop installation using cloud-init. It also automatically update fog pxe menu entry

    Since ubuntu 20, canonical deprecated debian-installer. Debian-installer is still working but I ran into issues with some statements and find out that using debian-installer successor cloud-init a bit easier to perform autoinstall.

    Ubuntu provide daily builds, we can use them to keep our iso updated by setting up a daily cronjob for this script and then add the user-data file needed as cloud-init input (inspired myself from this script which help greatly testing user-data file by wrapping it up into an ubuntu-server iso which can then be directly use either on vm or physical machine).

    #!/bin/bash
    
    DIST="ubuntu"
    RELEASE="focal"
    URL="https://cdimage.ubuntu.com/ubuntu-server/${RELEASE}/daily-live/current/${RELEASE}-live-server-amd64.iso"
    CHECKSUM_URL="https://cdimage.ubuntu.com/ubuntu-server/${RELEASE}/daily-live/current/SHA256SUMS"
    IMAGE_NAME="${DIST}/${RELEASE}"
    GIT_URL="https://some.git.repo.url/containing-user-data-file"
    
    # you need to setup an nfs server serving the NFS_DIR and a tftp server serving TFTP_DIR
    DOWNLOAD_DIR="/data/iso"
    TFTP_DIR="/tftpboot/os"
    NFS_DIR="/data/nfs"
    MOUNT_DIR="/media/tmp_image"
    NFS_SERVER_IP="your_fog_nfs_server_ip"
    
    # only define this if you have several vmlinuz or initrd inside the iso
    VMLINUZ=""
    INITRD=""
    
    # pxe parameters use in fog pxe menu entry 
    PXE_PARAMS="kernel tftp://\$\{fog-ip\}/os/${DIST}/${RELEASE}/vmlinuz
    initrd tftp://\$\{fog-ip\}/os/${DIST}/${RELEASE}/initrd
    imgargs boot=casper ip=dhcp netboot=nfs nfsroot=\$\{fog-ip\}:${NFS_DIR}/${IMAGE_NAME} rw"
    
    mkdir -p ${DOWNLOAD_DIR}/${DIST}/${RELEASE}
    image="${DOWNLOAD_DIR}/${DIST}/${RELEASE}/${RELEASE}-live-server-amd64.iso"
    
    function cleanup() {
            trap - SIGINT SIGTERM ERR EXIT
            if [ -n "${tmpdir+x}" ]; then
                    rm -rf "$tmpdir"
                    echo "🚽 Deleted temporary working directory $tmpdir"
            fi
    }
    
    trap cleanup SIGINT SIGTERM ERR EXIT
    
    testdir() {
      if [ ! -d "$1" ]; then
        echo "Directory $1 doesn't exist" 1>&2
        usage
        exit 1
      fi
    }
    
    checkIso() {
      if [ ! -f "${tmpdir}/SHA256SUMS-${today}" ]; then
        curl -NsSL ${CHECKSUM_URL} -o "${tmpdir}/SHA256SUMS-${today}"
      fi
      digest=$(sha256sum "${image}" | cut -f1 -d ' ')
      set +e
      grep -Fq "$digest" "${tmpdir}/SHA256SUMS-${today}"
      if [ $? -eq 0 ]; then
        echo "👍 Verification succeeded."
        set -e
        return 0
      else
        echo "👿 Verification of ISO digest failed." 1>&2;
        return 1
      fi
    }
    
    downloadIso() {
      if [ ! -f "${image}" ]; then
        echo "File $image doesn't exist, downloading iso"
        wget -P ${DOWNLOAD_DIR}/${DIST}/${RELEASE} ${URL}
      fi
      checkIso
      if [ $? -eq 1 ]; then
        rm ${image}
        downloadIso
      fi
    # TODO eventually check iso signature
    }
    
    # updating fog PXEMenu  
    updatePXEMenu() {
      echo "param $1 $2"
      pxeID=$(mysql --batch --skip-column-names --execute="select pxeID from fog.pxeMenu where pxeName=\"${DIST}_${RELEASE}_$1\"" | head -n 1)
      if [ -z "$pxeID"  ]; then
        echo "No entry for ${DIST} ${RELEASE} $1, generating a new one"
    
        mysql --batch --skip-column-names --execute="INSERT INTO fog.pxeMenu (pxeName, pxeDesc, pxeParams, pxeRegOnly, pxeArgs, pxeDefault, pxeHotKeyEnable, pxeKeySequence) VALUES (\"${DIST}_${RELEASE}_$1\", \"${DIST} ${RELEASE} $1 image\", '$2', 2, \"\", 0, \"0\", \"\");"
      else
        echo "Entry found for ${DIST} ${RELEASE}, updating"
        mysql --batch --skip-column-names --execute="UPDATE fog.pxeMenu SET pxeName=\"${DIST}_${RELEASE}_$1\", pxeDesc=\"${DIST} ${RELEASE} $1 image\", pxeParams='$2' where pxeID=${pxeID};"
      fi
    }
    
    # clone the git repo containing user-data files
    # it will search for user data store in /user-data/${DIST}/${RELEASE}/
    # you can them define as many user-data file as you want by creating storing 
    # them this way inside the repo : 
    # user-data/${DIST}/${RELEASE}/desktop-minimal/user-data
    # user-data/${DIST}/${RELEASE}/desktop-full/user-data 
    addUserData() {
      git clone --quiet ${GIT_URL} ${tmpdir}/cloud-init
      mkdir -p ${NFS_DIR}/${IMAGE_NAME}/nocloud
      cp -r ${tmpdir}/cloud-init/user-data/${DIST}/${RELEASE}/* ${NFS_DIR}/${IMAGE_NAME}/nocloud/
      for item in ${NFS_DIR}/${IMAGE_NAME}/nocloud/*; do
        item="$(echo $item | sed 's#.*/##')"
        tmp="${PXE_PARAMS} autoinstall ds=nocloud;s=/cdrom/nocloud/${item}"
        updatePXEMenu "autoinstall_${item}" "${tmp}"
      done
    }
    
    echo "PXE image script"
    if [ "$EUID" -ne 0 ]; then
      echo "Please run as root"
      exit
    fi
    
    tmpdir=$(mktemp -d)
    
    if [[ ! "$tmpdir" || ! -d "$tmpdir" ]]; then
            echo "💥 Could not create temporary working directory." 1>&2; exit 1;
    else
            echo "📁 Created temporary working directory $tmpdir"
    fi
    
    
    downloadIso
    mkdir -p ${MOUNT_DIR}
    echo "Trying to mount ${image} to ${MOUNT_DIR}..."
    mount -ro loop ${image} ${MOUNT_DIR}
    vmlinuzs=( $(find ${MOUNT_DIR} -name "vmlinuz*") )
    index=0
    nbelem=${#vmlinuzs[@]}
    if [ ${nbelem} -ne 1 ]; then
      echo "absent or multiple vmlinuz file, please run interactive script" 1>&2
      exit 1
    fi
    VMLINUZ=${vmlinuzs[${index}]}
    echo "The file ${VMLINUZ} will be used"
    initrds=( $(find ${MOUNT_DIR} -name "initrd*") )
    index=0
    nbelem=${#initrds[@]}
    if [ ${nbelem} -ne 1 ]; then
      echo "absent or multiple initrd file, please run interactive script" 1>&2
      exit 1
    fi
    INITRD=${initrds[${index}]}
    echo "The file ${INITRD} will be used"
    
    mkdir -vp ${TFTP_DIR}/${IMAGE_NAME}
    mkdir -vp ${NFS_DIR}/${IMAGE_NAME}
    echo "Copying the image file to NFS Server"
    cp -Rf ${MOUNT_DIR}/* ${NFS_DIR}/${IMAGE_NAME}
    # match hidden files too (store UUID needed to use NFS as it perform a check)
    cp -Rf ${MOUNT_DIR}/.[!.]* ${NFS_DIR}/${IMAGE_NAME}
    echo "Copying the vmlinuz & initrd files to TFTP Server"
    cp -f ${VMLINUZ} ${TFTP_DIR}/${IMAGE_NAME}/
    cp -f ${INITRD} ${TFTP_DIR}/${IMAGE_NAME}/
    umount ${MOUNT_DIR}
    
    echo "Editing TFTP config file"
    # setting up an entry with no autoinstall 
    updatePXEMenu "server" "${PXE_PARAMS}"
    # setting up one entry per user-data stored in your git repo 
    addUserData
    echo "Restarting TFTP Server"
    service tftpd-hpa restart
    echo "Finished"
    
    

    To setup manually this instead of using this script, you can follow this kind of instruction but perform the following command after this one cp -R /mnt/loop/* /images/os/mint/19.1

    cp -R /mnt/loop/.[!.]* /images/os/mint/19.1
    

    This script can be adapted to setup other distro iso (without automation as I think only ubuntu will work with cloud init)

280
Online

9.5k
Users

15.9k
Topics

147.5k
Posts