Over Your Local Network, Automatically Flash Raspberry Pi SD Cards - Raspberry Pi Projects, Tutorials, Learning DIY Electronics - Makergenix

Breaking

 


Over Your Local Network, Automatically Flash Raspberry Pi SD Cards

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.

Over Your Local Network, Automatically Flash Raspberry Pi SD Cards

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=Standard
 
if [ -z "$1" ]
then
    echo "ERROR: I need one argument: the raspi image file"
    exit 1
fi
 
if [ ! -f "$1" ]; then
    echo "ERROR: Image file $1 doesn't exist"
    exit 1
fi
 
echo "> installing needed packages"
apt-get update > /dev/null
apt-get install -y bc nfs-kernel-server tftpd-hpa apache2 php php-curl php-xml libapache2-mod-php ipcalc curl wget screen lsof > /dev/null
 
myip=`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 start
echo ">   nfs"
service nfs-kernel-server start
echo ">   web"
service apache2 start
 
echo "> 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.sh
disk_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 1
fi
 
echo "> writing image to disk"
disk=\`lsblk -d -l -p -n | cut -d' ' -f1\`
dd if=/tmp/img of=\$disk bs=1M status=progress
 
if [ \$? -ne 0 ]
then
    echo "> ERROR: writing image to disk failed"
    echo ">   rebooting in 300 seconds"
    sleep 300
    reboot
    exit 1
fi
 
echo "> rebooting in 10 seconds"
sleep 10
wget -qO- http://${myip}/reboot.php &> /dev/null
reboot
EOF
 
 
#  _           _                   _
# (_)_ __   __| | _____  __  _ __ | |__  _ __
# | | '_ \ / _` |/ _ \ \/ / | '_ \| '_ \| '_ \
# | | | | | (_| |  __/>  < _| |_) | | | | |_) |
# |_|_| |_|\__,_|\___/_/\_(_) .__/|_| |_| .__/
#                           |_|         |_|
#
echo ">   index.php"
cat << EOF > /var/www/html/index.php
<?php
 
echo "Hi!" ;
exit( 0 ) ;
 
?>
EOF
 
 
#  _                         _
# (_)_ __ ___   __ _   _ __ | |__  _ __
# | | '_ ` _ \ / _` | | '_ \| '_ \| '_ \
# | | | | | | | (_| |_| |_) | | | | |_) |
# |_|_| |_| |_|\__, (_) .__/|_| |_| .__/
#              |___/  |_|         |_|
echo ">   img.php"
cat << EOF > /var/www/html/img.php
<?php
 
header( "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
<?php
 
echo 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\"" ) ;
 
?>
EOF
 
echo "> 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/null
mount -o loop,offset=$real_boot_offset $1 /tmp/boot
rm -rf /srv/tftp/*
cp -rpf /tmp/boot/* /srv/tftp/
umount /tmp/boot
 
mkdir /tmp/root 2>/dev/null
mount -o loop,offset=$real_linux_offset $1 /tmp/root
mkdir /srv/nfs 2>/dev/null
cp -rpf /tmp/root/* /srv/nfs/
 
echo "> getting sdboot eeprom ready"
cat << EOF > /tmp/eeprom_config.sdboot
[all]
DHCP_TIMEOUT=45000
DHCP_REQ_TIMEOUT=4000
TFTP_FILE_TIMEOUT=30000
TFTP_IP=${myip}
TFTP_PREFIX=1
TFTP_PREFIX_STR=
BOOT_ORDER=0xf21
ENABLE_SELF_UPDATE=1
SD_BOOT_MAX_RETRIES=3
NET_BOOT_MAX_RETRIES=2
EOF
 
latest_eeprom=`ls -1 /tmp/root/lib/firmware/raspberrypi/bootloader/stable/pieeprom-*.bin | sort -u | tail -1`
cp $latest_eeprom /tmp/pieeprom.bin
rpi-eeprom-config --out /tmp/pieeprom-out.bin --config /tmp/eeprom_config.sdboot /tmp/pieeprom.bin && rpi-eeprom-update -d -f /tmp/pieeprom-out.bin
mv /boot/pieeprom.sig /srv/tftp/pieeprom.sig.inert
mv /boot/pieeprom.upd /srv/tftp/pieeprom.upd.inert
mv /boot/recovery.bin /srv/tftp/recovery.bin.inert
 
cat << EOF > /usr/local/bin/enable_eeprom_sdboot.sh
sed -i "s/ts:.*/ts: \`date +%s\`/g" /srv/tftp/pieeprom.sig.inert
mv /srv/tftp/pieeprom.sig.inert /srv/tftp/pieeprom.sig
mv /srv/tftp/pieeprom.upd.inert /srv/tftp/pieeprom.upd
mv /srv/tftp/recovery.bin.inert /srv/tftp/recovery.bin
EOF
chmod 755 /usr/local/bin/enable_eeprom_sdboot.sh
 
cat << EOF > /usr/local/bin/disable_eeprom_sdboot.sh
mv /srv/tftp/pieeprom.sig /srv/tftp/pieeprom.sig.inert
mv /srv/tftp/pieeprom.upd /srv/tftp/pieeprom.upd.inert
mv /srv/tftp/recovery.bin /srv/tftp/recovery.bin.inert
EOF
chmod 755 /usr/local/bin/disable_eeprom_sdboot.sh
 
cat << EOF > /etc/sudoers.d/010_www-data_eeprom
www-data    ALL=(ALL) NOPASSWD: /usr/local/bin/enable_eeprom_sdboot.sh
www-data    ALL=(ALL) NOPASSWD: /usr/local/bin/disable_eeprom_sdboot.sh
EOF
 
umount /tmp/root
 
echo "/srv/nfs    $mynetwork(rw,sync,no_subtree_check,no_root_squash)" > /etc/exports
exportfs -rav
 
echo "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.txt
 
echo "proc /proc proc defaults 0 0" > /srv/nfs/etc/fstab
 
echo "> deploying image"
if [ ! -f /var/img ]; then
    cp -f $1 /var/img
else
    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.old
fi
 
echo "> adding web bootstrap"
echo '#!/bin/sh -e' > /srv/nfs/etc/rc.local
echo "echo \">   disabling cron\"" >> /srv/nfs/etc/rc.local
# I've seen this command hang several times before
echo "/usr/bin/timeout --kill-after=10s 5s /usr/sbin/service cron stop || :" >> /srv/nfs/etc/rc.local
echo "echo \">   sleeping until we have connectivity\"" >> /srv/nfs/etc/rc.local
echo "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.local
echo "echo \">   retrieving imaging image\"" >> /srv/nfs/etc/rc.local
echo "curl -s http://${myip}/img.php --output /tmp/img" >> /srv/nfs/etc/rc.local
echo "echo \">   launching bootstraping process\"" >> /srv/nfs/etc/rc.local
echo "curl -s http://${myip}/bootstrap.sh --output /tmp/bootstrap.sh" >> /srv/nfs/etc/rc.local
echo "bash /tmp/bootstrap.sh" >> /srv/nfs/etc/rc.local
 
echo "> 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/bash
 
cat << EOF > /tmp/eeprom_config.netboot
[all]
DHCP_TIMEOUT=45000
DHCP_REQ_TIMEOUT=4000
TFTP_FILE_TIMEOUT=30000
TFTP_IP=192.168.1.116
TFTP_PREFIX=1
TFTP_PREFIX_STR=
BOOT_ORDER=0xf12
ENABLE_SELF_UPDATE=1
SD_BOOT_MAX_RETRIES=3
NET_BOOT_MAX_RETRIES=2
EOF
 
/usr/bin/rpi-eeprom-config --apply /tmp/eeprom_config.netboot
sleep 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

 


Most Viewed Posts

Write For Us

Name

Email *

Message *

All Blogs