Script for burning SD cards for Pis

Not specifically node-red oriented, but I have written a script that I use to burn SD cards for Pis, and it was suggested I post it here. It burns one of the standard Raspbian downloads then sets up the wifi SSID and password if required, IP address, and DNS, enables ssh and renames the default user. The idea is that it should be possible to plug the card into the Pi and it should power up and be accessible without further work. I then use Ansible to install and configure node-red and other packages as required but that is a separate issue.
It is written in ruby but could easily be rewritten in your language of choice.
It runs on Ubuntu, but should not need much work to run on a different Linux. Windows would be a bit tricker I think.
If you try it be careful to pay attention to the messages before telling it to do the burn. It shows you the mounted devices so you can ensure that you are burning to the correct device and asks for confirmation twice, but if you tell it to write to the wrong device then it may overwrite your system disk. So please convince yourself it is doing the right thing before running it. I accept no liability for any disasters that may ensue.

#!/usr/bin/env ruby
require 'io/console'

# burn and build a pi SD card given
#           raspbian image zip file, name of pi to build and sd_device (eg sdb or mmcblk0)

default_user = "userx"
default_router = "192.168.1.1"
default_ssid = "MySSID"
default_dns = default_router
devices = Hash[
  "piz002" => {:interface => "wlan0", :ip => "192.168.1.81", :dns => "192.168.1.1", 
    :user => "userx", :router => "192.168.1.1", :ssid => "MySSID"},
  "pixxx" => {:interface => "wlan0", :ip => "192.168.1.20", :dns => "192.168.1.1", :user => "userx"},
  "pi003" => {:interface => "wlan0", :ip => "192.168.1.82"}
]

def configure_device( name, interface, ip, user, dns, router, ssid )
  puts "Enter wifi password"
  password = STDIN.noecho(&:gets).chomp
  # probably don't need sudo here as script run with sudo, also probably don't need the chmod
  #system("chmod 664 /media/userx/rootfs/etc/wpa_supplicant/wpa_supplicant.conf")
  system("echo 'country=GB' >> /media/userx/rootfs/etc/wpa_supplicant/wpa_supplicant.conf")
  system("echo 'network={' >> /media/userx/rootfs/etc/wpa_supplicant/wpa_supplicant.conf")
  system("echo '  ssid=\"#{ssid}\"' >> /media/userx/rootfs/etc/wpa_supplicant/wpa_supplicant.conf")
  system("echo '  psk=\"#{password}\"' >> /media/userx/rootfs/etc/wpa_supplicant/wpa_supplicant.conf")
  system("echo '  key_mgmt=WPA-PSK' >> /media/userx/rootfs/etc/wpa_supplicant/wpa_supplicant.conf")
  system("echo '}' >> /media/userx/rootfs/etc/wpa_supplicant/wpa_supplicant.conf")
  #system("chmod 644 /media/userx/rootfs/etc/wpa_supplicant/wpa_supplicant.conf")
  # add interface to /etc/dhcpcd.conf
  system("echo 'interface #{interface}' >> /media/userx/rootfs/etc/dhcpcd.conf")
  system("echo 'static ip_address=#{ip}/24' >> /media/userx/rootfs/etc/dhcpcd.conf")
  system("echo 'static routers=#{router}' >> /media/userx/rootfs/etc/dhcpcd.conf")
  system("echo 'static domain_name_servers=#{dns}' >> /media/userx/rootfs/etc/dhcpcd.conf")
  # change hostname in /etc/hosts and /etc/hostname
  ["/media/userx/rootfs/etc/hosts","/media/userx/rootfs/etc/hostname"].each do |file_name|
    system("sed -i 's/raspberrypi/#{name}/g' #{file_name}")
  end
  # change default username from pi to user
  ["/media/userx/rootfs/etc/passwd", "/media/userx/rootfs/etc/shadow"].each do |file_name|
    cmd="sed -i 's/pi:/#{user}:/g' #{file_name}"
    system("echo '#{cmd}'")
    system("sed -i 's/pi:/#{user}:/g' #{file_name}")
  end
  system("sed -i 's/:pi$/:#{user}/g' /media/userx/rootfs/etc/group")
  system("sed -i 's/^pi:/#{user}:/g' /media/userx/rootfs/etc/group")
  # rename home folder
  system("mv /media/userx/rootfs/home/pi /media/userx/rootfs/home/#{user}")
  # enable ssh
  system( "touch /media/userx/boot/ssh" )
  # make ssh keys
  system("mkdir /media/userx/rootfs/home/#{user}/.ssh")
  system("ssh-keygen -N '' -t rsa -f /media/userx/rootfs/home/#{user}/.ssh/id_rsa")
  # change ownership to user
  system("chown -R #{user}:#{user} /media/userx/rootfs/home/userx/.ssh")
end

def burn( src, sd_device )
  answer = false
  puts `lsblk|grep 'sd\\|mmc'`
  puts "Check the SD card is #{sd_device}.  Ok y/n?"
  ans = STDIN.gets.chomp    # have to use STDIN to stop it picking up the command line
  if ans == 'y'
    puts "Are you sure you want to burn #{src} to #{sd_device}? this will ERASE #{sd_device}!!!"
    ans = STDIN.gets.chomp
    if ans == 'y'
      puts "Burning #{sd_device} from #{src}"
      cmd = "unzip -p #{src} |dd of=/dev/#{sd_device} bs=4M status=progress conv=fsync"
      answer = system( cmd )
    end
  end
  return answer
end


if ENV["USER"] != "root"
  puts "Script must be run with sudo"
  exit
end

# should be exactly three parameters
if ARGV.length != 3 then
  puts "Usage: sudo pi_burn_and_rebuild   zipfile   pi_id sd_device (eg sdb or mmcblk0)"
  puts "Where zipfile is the raspbian image zip file and pi_id is pi to burn, eg pi002"
else
  host_name = ARGV[1]
  sd_device = ARGV[2]
  device = devices[host_name]
  # check device exists in table
  if !device
    puts "Unknown device #{host_name}"
  else
    user = device[user] ? device[user] : default_user
    dns = device[dns] ? device[dns] : default_dns
    router = device[router] ? device[router] : default_router
    ssid = device[ssid] ? device[ssid] : default_ssid
    puts "device: #{device}, user: #{user}, dns: #{dns}, router: #{router}, ssid: #{ssid}"
    if burn(ARGV[0], sd_device)
      puts  "Burn succeeded",
            "Unplug the card, wait a few seconds then plug in again, wait till it mounts then hit enter"
      STDIN.gets
      configure_device( host_name, device[:interface], device[:ip], user, dns, router, ssid )
      puts " ",
       "Now unmount the SD card.",
       " ",
       "Don't forget to change the password (passwd) from raspberry after connecting as #{user}"
    else
      puts "Burn failed or aborted"
    end
  end
end
8 Likes

Might try and redo that in Node.js sometime.

For Windows, I have a PowerShell script that does something similar but also installs some standard software. Always seems to be out of date when I try to run in though so I usually run bits manually and add the missing bits if I can be bothered. More of an aide memoir really.

:slightly_smiling_face: or a linux shell script would be nice. I already maintain things in too many languages...

1 Like

Colin, I see you have default_user “userx” configurable, but paths like “/media/userx” hardcoded. Is that done on purpose?

Nice work either way, and if you have your ansible playbooks available too it would make me really happy :slight_smile:

The advantage of doing it in JavaScript is that it will be cross-platform.

1 Like

In my use case /media/userxx is always my user name, as that is the currently logged in use on the machine running the script, whereas the user defined in the machine data structure (defaulting to default_user) will be the end user on that machine, which in my use case is usually me too, but might not be. The script could probably pick up the current logged in user to use in /media, but I didn't need that.
My nodejs install and node-red install ansible tasks might be of interest, I will look at sorting them out.

I think Ruby is cross platform too isn't it? What makes is not cross platform is using system commands such as dd and sed I think. Or can those be used in the Linux in Windows stuff, which I don't know anything about? Also where the card is mounted would be different.

If have setup a Github repo with the tasks I use for installing nodejs and node-red on a Pi or other Debian based system (including Pi Zero which needs a different build of nodejs). I have not been using Ansible long so no doubt it is full of holes and inefficiencies. I will be glad to get any suggestions on how to improve it. Do let me know if anyone finds it useful, it is always good to know.
I did look at the offerings various others have produced to do the tasks, but none of them seemed to quite do what I wanted.
I haven't actually tested it exactly as it is, as I tweaked it a bit to take out some hard coded stuff before releasing, so no guarantees.

3 Likes

It looks pretty clean to me, thanks :smiley:

Yes, Ruby is cross-platform but I don't use Ruby. I do use JavaScript so with the exception of certain things that only PowerShell does well to do with Windows Systems, I will always try to use JS over anything else. Which is why I gave up doing Python, PHP, PERL, etc. Of course, sometimes using a shell language is much quicker for one-off tasks but for anything that I might want to reuse, I do try to stick with JS.

Well, you can certainly get things like dd and sed for Windows but it generally isn't worth it. I could always use the Windows Linux Subsystem (WSL) if I need that kind of thing. There are usually JS libraries that will do the equivalent.

Drive mounts are differently labelled but actually Node.js is really good at sorting that out for you using path.join. If not, again there will be tricks for getting the info you need.

Easy peasy then. I look forward to seeing a js version :slight_smile:

I did initially start trying to do it in js (as I am trying to move over to that) but realised that I just needed to get it going and it was going to be much quicker for me in Ruby.

1 Like

Yup, it is on the bottom of my task list :rofl:

How odd. It seems like my list is always LIFO.