My day job is authoring production-grade software. Often that has the side effect of creating abstract, reusable components. Creating components like this can take a lot of experience, skill, and brainpower. So by the end of the day, when I’m tired and want to automate some annoying task for my homelab, it feels really good to kick out some cheapie, ugly, barely maintainable, non-reusable, and possibly raunchy shell script.
Here I’ll describe a small set of zsh
scripts to apt-get
update several Ubuntu installations I have throughout the homelab.
The chore
Right now, I have about a half dozen Ubuntu virtual machines or LXCs running across two Proxmox hosts. I also have two Raspberry Pis — with a third coming soon — running as independent hosts on the network. I use these VMs, LXCs, and Rasberry Pis for running various services and tinkering.
On each system, I’ve given my user fletcher
sudo
permissions to run the apt-get
upgrade suite of commands to upgrade packages. Each system also has an alias to run all the commands. But logging into each system and running the alias is annoying, so I used ChatGPT to help me make some zsh scripts to update all the systems in one go.
The scripts
There are three zsh scripts in total. Let’s start with the first two.
First is mass_upgradez.zsh
:
#!/usr/bin/env zsh
here=$(dirname "${${(%):-%x}:A}")
typeset -A servers
servers=(
fletcher "trajan hadrian nero"
root "claudius"
)
source "$here/update_servers.zsh"
This script, in turn, delegates to update_servers.zsh
:
#!/usr/bin/env zsh
here=$(dirname "$0")
upgradez="export DEBIAN_FRONTEND=noninteractive; sudo -E apt-get update && sudo -E apt-get upgrade -y && sudo apt-get autoclean && sudo apt-get autoremove -y"
for user in ${(k)servers}; do
for server in ${(s: :)servers[$user]}; do
echo "Updating [$server] with user [$user]."
ssh "$user"@"$server" "$upgradez"
echo
done
done
echo "Done."
The first script starts by defining here
— which is an absolute path to the directory this script resides in. This is a much nicer way to say “here” than dirname "${${(%):-%x}:A}"
. Arcane shell script syntax is something I rarely need, and therefore have no chance of remembering, and this is where ChatGPT comes in so handy. It gave me the "${${(%):-%x}:A}"
funny business, and I am grateful ChatGPT can remember this so I don’t need to.
Then, mass_upgradez.zsh
defines an associative array using the typeset
keyword and syntax. In this array, the keys are the name of the user account I need to use, and the value is a list of systems that have that username with sudo
permissions. In this case, one system (the LXC) uses root
, and the rest use fletcher
.
The script concludes by delegating control to the update_servers.zsh
script, which is located alongside this one.
Running the updates
Believe it or not, update_servers.zsh
is responsible for running the updates. First, it defines a variable to hold the chain of update and upgrade commands. This set of variables, commands, and flags should minimize the change that apt-get
pops up any dialogs or prompts.
Then the script loops over the users and servers. For each user-server pair, it uses ssh
to run the upgrade commands. Lastly, this script uses echo
to output some text and make the overall output a little more readable.
Reusing the delegate script
In case you’re wondering why we have two scripts here instead of one — it is because I want to reuse the update_servers.zsh
script in another context.
Here is another script I have for upgrading the Raspberry Pis: pi_upgradez.zsh
:
#!/usr/bin/env zsh
here=$(dirname "${${(%):-%x}:A}")
typeset -A servers
servers=(
fletcher "augustus paullus"
)
source "$here/update_servers.zsh"
This one looks a lot like the first script, and that’s intentional — it is almost the same, aside from the list of servers. In this case, these are my Raspberry Pis. When I want to update my VMs and LXCs, I use the first script, when I want to update my Raspberry Pis, I use this one.
Ok, but why three scripts instead of one?
Yup, I could put all of this into one script. That would be trivial.
The reason why I separate the VMs and LXCs from the Raspberry Pis is because of speed. The VMs and LXCs are running on high-powered, modern Intel platforms. They can all update pretty quickly. But in my case, at least one of the Raspberry Pis is a Raspberry Pi 2, which is slow. (The only thing it does is DNS, so it’s good at that at least.) Both Raspberry Pis, but especially the 2, are slow to run all the updates. I only run this script when I know I’ll have plenty of time to let it finish.
Therefore I have three scripts: one for the fast computers, one for the slow computers, and common logic shared between the two.
Sometimes quick and dirty wins
As per usual, the shell scripts are appalling to look at and try to read. But they get the job done for my limited needs. And thanks to ChatGPT I don’t need to memorize the rain dance of obscure symbols to program the script. All-in-all I consider this set of scripts a big win.