This excellent technique, which is now being used to handle a 70-node Raspberry Pi deployment, makes keeping your OSes up to date as simple as possible.
Ben Akrin, a Raspberry Pi guru, has put up a tutorial for automated, fully-headless imaging, with the goal of getting Raspberry Pi single-board computers up and running as soon as possible and with as little human input as feasible.
Simply connect two Raspberry Pis to the same network. The Netboot Server provides images to other Pis on the network, whereas the Pi to image will request an image from the Netboot Server and write it to its own SD card. The Netboot Server doesn't have to be another Pi, and it doesn't require any special Pi sauce, but I like to use Pis for everything. The Pi to be imaged, on the other hand, must also be a Pi; we're imaging Pis here.
Configuring the NetBoot Server
Any current vanilla RaspiOS server may be used. Under /home/pi/script.sh, copy the following script.
#!/bin/bash# ascii text from https://patorjk.com/software/taag/#p=display&f=Standardif [ -z "$1" ]then echo "ERROR: I need one argument: the raspi image file" exit 1fiif [ ! -f "$1" ]; then echo "ERROR: Image file $1 doesn't exist" exit 1fiecho "> installing needed packages"apt-get update > /dev/nullapt-get install -y bc nfs-kernel-server tftpd-hpa apache2 php php-curl php-xml libapache2-mod-php ipcalc curl wget screen lsof > /dev/nullmyip=`ifconfig | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1'`mymask=`ifconfig | grep $myip | grep -Eo 'netmask (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1'`mynetwork=`ipcalc $myip/$mymask | grep "Network:" | sed 's/\s\{1,\}/ /g' | cut -d' ' -f2`echo "> my ip: $myip"echo "> my network: $mynetwork"echo "> starting services"echo "> tftp"service tftpd-hpa startecho "> nfs"service nfs-kernel-server startecho "> web"service apache2 startecho "> clean slate"rm -rf /srv/tftp/*rm -rf /srv/nfs/*rm -rf /var/www/html/*echo "> installing web components"echo "> bootstrap.sh"# _ _ _ _# | |__ ___ ___ | |_ ___| |_ _ __ __ _ _ __ ___| |__# | '_ \ / _ \ / _ \| __/ __| __| '__/ _` | '_ \ / __| '_ \# | |_) | (_) | (_) | |_\__ \ |_| | | (_| | |_) |\__ \ | | |# |_.__/ \___/ \___/ \__|___/\__|_| \__,_| .__(_)___/_| |_|# |_|cat << EOF > /var/www/html/bootstrap.shdisk_count=\`lsblk -d -l -p -n | cut -d' ' -f1 | wc -l\`if [ \$disk_count -ne 1 ]then echo "ERROR: I need to have exactly 1 disk to write to" exit 1fiecho "> writing image to disk"disk=\`lsblk -d -l -p -n | cut -d' ' -f1\`dd if=/tmp/img of=\$disk bs=1M status=progressif [ \$? -ne 0 ]then echo "> ERROR: writing image to disk failed" echo "> rebooting in 300 seconds" sleep 300 reboot exit 1fiecho "> rebooting in 10 seconds"sleep 10wget -qO- http://${myip}/reboot.php &> /dev/nullrebootEOF# _ _ _# (_)_ __ __| | _____ __ _ __ | |__ _ __# | | '_ \ / _` |/ _ \ \/ / | '_ \| '_ \| '_ \# | | | | | (_| | __/> < _| |_) | | | | |_) |# |_|_| |_|\__,_|\___/_/\_(_) .__/|_| |_| .__/# |_| |_|#echo "> index.php"cat << EOF > /var/www/html/index.php<?phpecho "Hi!" ;exit( 0 ) ;?>EOF# _ _# (_)_ __ ___ __ _ _ __ | |__ _ __# | | '_ ` _ \ / _` | | '_ \| '_ \| '_ \# | | | | | | | (_| |_| |_) | | | | |_) |# |_|_| |_| |_|\__, (_) .__/|_| |_| .__/# |___/ |_| |_|echo "> img.php"cat << EOF > /var/www/html/img.php<?phpheader( "Cache-Control: no-store, no-cache, must-revalidate" ) ;header( "Cache-Control: post-check=0, pre-check=0", false ) ;header( "Pragma: no-cache" ) ;header( "Expires: ".gmdate("D, d M Y H:i:s", mktime(date("H")+2, date("i"), date("s"), date("m"), date("d"), date("Y")))." GMT" ) ;header( "Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT" ) ;header( "Content-Type: application/octet-stream" ) ;header( "Content-Length: ".(string)(filesize("/var/img")) ) ;header( "Content-Transfer-Encoding: binary\n" ) ;\$handle = fopen( "/var/img", "rb" ) ;if( \$handle===false ) { echo "can't read image" ; exit( 1 ) ;}while( !feof(\$handle) ) { echo fread( \$handle, 8192 ) ;}fclose( \$handle ) ;?>EOF# _ _ _# _ __ ___| |__ ___ ___ | |_ _ __ | |__ _ __# | '__/ _ \ '_ \ / _ \ / _ \| __| | '_ \| '_ \| '_ \# | | | __/ |_) | (_) | (_) | |_ _| |_) | | | | |_) |# |_| \___|_.__/ \___/ \___/ \__(_) .__/|_| |_| .__/# |_| |_|echo "> reboot.php"cat << EOF > /var/www/html/reboot.php<?phpecho shell_exec( "sudo /usr/local/bin/enable_eeprom_sdboot.sh 2>&1; screen -S disable_eeprom_sdboot -dm sh -c \"sleep 240; sudo /usr/local/bin/disable_eeprom_sdboot.sh\"" ) ;?>EOFecho "> dissecting image file"offset=`fdisk -l $1 | grep Linux | tr -s ' ' | tr '\t' ' ' | cut -d' ' -f2`real_linux_offset=`echo "$offset*512" | bc`offset=`fdisk -l $1 | grep W95 | tr -s ' ' | tr '\t' ' ' | cut -d' ' -f2`real_boot_offset=`echo "$offset*512" | bc`mkdir /tmp/boot 2>/dev/nullmount -o loop,offset=$real_boot_offset $1 /tmp/bootrm -rf /srv/tftp/*cp -rpf /tmp/boot/* /srv/tftp/umount /tmp/bootmkdir /tmp/root 2>/dev/nullmount -o loop,offset=$real_linux_offset $1 /tmp/rootmkdir /srv/nfs 2>/dev/nullcp -rpf /tmp/root/* /srv/nfs/echo "> getting sdboot eeprom ready"cat << EOF > /tmp/eeprom_config.sdboot[all]DHCP_TIMEOUT=45000DHCP_REQ_TIMEOUT=4000TFTP_FILE_TIMEOUT=30000TFTP_IP=${myip}TFTP_PREFIX=1TFTP_PREFIX_STR=BOOT_ORDER=0xf21ENABLE_SELF_UPDATE=1SD_BOOT_MAX_RETRIES=3NET_BOOT_MAX_RETRIES=2EOFlatest_eeprom=`ls -1 /tmp/root/lib/firmware/raspberrypi/bootloader/stable/pieeprom-*.bin | sort -u | tail -1`cp $latest_eeprom /tmp/pieeprom.binrpi-eeprom-config --out /tmp/pieeprom-out.bin --config /tmp/eeprom_config.sdboot /tmp/pieeprom.bin && rpi-eeprom-update -d -f /tmp/pieeprom-out.binmv /boot/pieeprom.sig /srv/tftp/pieeprom.sig.inertmv /boot/pieeprom.upd /srv/tftp/pieeprom.upd.inertmv /boot/recovery.bin /srv/tftp/recovery.bin.inertcat << EOF > /usr/local/bin/enable_eeprom_sdboot.shsed -i "s/ts:.*/ts: \`date +%s\`/g" /srv/tftp/pieeprom.sig.inertmv /srv/tftp/pieeprom.sig.inert /srv/tftp/pieeprom.sigmv /srv/tftp/pieeprom.upd.inert /srv/tftp/pieeprom.updmv /srv/tftp/recovery.bin.inert /srv/tftp/recovery.binEOFchmod 755 /usr/local/bin/enable_eeprom_sdboot.shcat << EOF > /usr/local/bin/disable_eeprom_sdboot.shmv /srv/tftp/pieeprom.sig /srv/tftp/pieeprom.sig.inertmv /srv/tftp/pieeprom.upd /srv/tftp/pieeprom.upd.inertmv /srv/tftp/recovery.bin /srv/tftp/recovery.bin.inertEOFchmod 755 /usr/local/bin/disable_eeprom_sdboot.shcat << EOF > /etc/sudoers.d/010_www-data_eepromwww-data ALL=(ALL) NOPASSWD: /usr/local/bin/enable_eeprom_sdboot.shwww-data ALL=(ALL) NOPASSWD: /usr/local/bin/disable_eeprom_sdboot.shEOFumount /tmp/rootecho "/srv/nfs $mynetwork(rw,sync,no_subtree_check,no_root_squash)" > /etc/exportsexportfs -ravecho "console=serial0,115200 console=tty1 root=/dev/nfs nfsroot="${myip}":/srv/nfs,nfsvers=3 ip=dhcp rw elevator=deadline fsck.repair=yes rootwait" > /srv/tftp/cmdline.txtecho "proc /proc proc defaults 0 0" > /srv/nfs/etc/fstabecho "> deploying image"if [ ! -f /var/img ]; then cp -f $1 /var/imgelse cp -f $1 /var/img.new if [ "`lsof | grep "/var/img" | wc -l`" -ne 0 ]; then echo "> waiting for file handle release on /var/img" while [ "`lsof | grep "/var/img" | wc -l`" -ne 0 ]; do echo -n "." sleep 1 done echo "" fi mv /var/img /var/img.old mv /var/img.new /var/img rm /var/img.oldfiecho "> adding web bootstrap"echo '#!/bin/sh -e' > /srv/nfs/etc/rc.localecho "echo \"> disabling cron\"" >> /srv/nfs/etc/rc.local# I've seen this command hang several times beforeecho "/usr/bin/timeout --kill-after=10s 5s /usr/sbin/service cron stop || :" >> /srv/nfs/etc/rc.localecho "echo \"> sleeping until we have connectivity\"" >> /srv/nfs/etc/rc.localecho "bash -c 'count=0; res=1; while [ \$res -ne 0 -a \$count -lt 120 ]; do echo "."; wget -q --spider http://${myip}; res=\$?; sleep 1; count=\$((count+1)); done'" >> /srv/nfs/etc/rc.localecho "echo \"> retrieving imaging image\"" >> /srv/nfs/etc/rc.localecho "curl -s http://${myip}/img.php --output /tmp/img" >> /srv/nfs/etc/rc.localecho "echo \"> launching bootstraping process\"" >> /srv/nfs/etc/rc.localecho "curl -s http://${myip}/bootstrap.sh --output /tmp/bootstrap.sh" >> /srv/nfs/etc/rc.localecho "bash /tmp/bootstrap.sh" >> /srv/nfs/etc/rc.localecho "> all done"Make is a program that may be run with:
1 | chmod 755 /home/pi/script.sh |
Download a RaspiOS image, the one you wish to deliver to the Pi, and save it to /home/pi on the server.
Use the RaspiOS image as the parameter in the script:
1 | sudo /home/pi/script.sh /home/pi/2022-01-28-raspios-bullseye-armhf-lite.img |
Depending on the speed of your SD card, this may take some time. The IP address of our NetBoot Server is 192.168.1.116.
Reimaging the Pi by sending it to the image
Once your server is up and running, you may tell the Pi to reimage itself by telling it to network boot from 192.168.1.116. The following script will change the EEPROM to reflect this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #!/bin/bashcat << EOF > /tmp/eeprom_config.netboot[all]DHCP_TIMEOUT=45000DHCP_REQ_TIMEOUT=4000TFTP_FILE_TIMEOUT=30000TFTP_IP=192.168.1.116TFTP_PREFIX=1TFTP_PREFIX_STR=BOOT_ORDER=0xf12ENABLE_SELF_UPDATE=1SD_BOOT_MAX_RETRIES=3NET_BOOT_MAX_RETRIES=2EOF/usr/bin/rpi-eeprom-config --apply /tmp/eeprom_config.netbootsleep 1/usr/sbin/reboot |
Reimaging the Pi by sending it to the image
Once your server is up and running, you may tell the Pi to reimage itself by telling it to network boot from 192.168.1.116. The following script will change the EEPROM to reflect this:
A RaspiOS image is grabbed and served over NFS by a script on the NetBoot Server. In addition, it hosts a TFTP server that connects to the NFS share. You could operate your Pis totally off the network and ignore the SD card, but I think it's more practical to simply burn the image locally on the Pi's SD card and run it. In /etc/rc.local, the image includes instructions for obtaining the original RaspiOS image through HTTP and burning it on the SD card. It reboots after being burned, but not before notifying the NetBoot Server that it is doing so. This is because the NetBoot Server will have to catch that Pi restarting and ask it to return to booting from its newly burned SD card for a little period of time.
While the Pi is reimaging, you may see issues; this is because we're providing a whole OS via NFS, and parts of its filesystems are read-only. We just need enough kernel to perform wget, dd, and reboot, so that's ok. A better solution could be to serve a more purposed OS over NFS, but in this instance I like providing the OS we're imaging, not only because it's more convenient, but also because it provides me access to the important EEPROM binaries.
Source: Ben Akrin



.png)


