Random Musings

O for a muse of fire, that would ascend the brightest heaven of invention!


Side-loading FreeBSD versions using Boot Environments

Tuesday, 23 Feb 2021 Tags: freebsdhacksupgrade

In other words, upgrading a FreeBSD box the dirty way.

This is very much a “works on my machine” approach but it should get you 90% of the way to dealing with your own. Expect annoying breakages and minor fixes along the way.

The scenario is that we have, for some reason, a system that can’t be upgraded the normal way - perhaps it’s FreeBSD 13.0-BETA1 where upgrades were broken, or you are planning to side-load a RELEASE version while your main system still runs CURRENT.

Instead of running the installer like a sensible person, we’ll use the bectl(8) features to provide a Boot Environment, and do a dirty upgrade inside that, out of the way of the current system.

This means we can re-use any zfs datasets across both “installs”, such as zroot/usr/home and zroot/var/db/... and FreeBSD’s zfs dataset layout will just Do The Right Thing in most cases. Brilliant!

There are a few tricks and traps, but the main one is that /etc and similar config dirs will not quite line up between the current setup and whatever we are sideloading.

scenario

Like any decent FreeBSD developer, I am eating my own delicious dog-food, and running FreeBSD current - today it’s 14.0-CURRENT. But I also need to test out changes for release versions of FreeBSD, so here the version I want to side-load is 13.0-BETA1 although by the time you read this, it will be BETA4 and soon RC1. It’s hard to keep up.

things we want to keep

  • /boot/loader.conf(8)
  • anything in our EFI partition
  • /etc/passwd, /etc/group and related files
  • /etc/shells
  • /etc/rc.conf
  • /var/unbound this is softlinked in /etc/unbound but because local-unbound(8) runs in a chroot, it can’t be present in /etc
  • missing /etc/fstab mountpoints that aren’t part of default install
  • a list of leaf packages via pkg prime-origins > /etc/packages.list
  • a handful of other similar files
  • /home softlink

stash our configs

I use git to keep track of any local changes to /etc and similar dirs. This isn’t necessary but you end up with more fine-grained history than just zfs snapshots, and you’ll see that later on, when we want to “merge in” the changes in /etc/rc.d and similar files, it makes this process very very easy.

export NOW=`date -u +%Y%m%d-%H%M`
export PAGER="/bin/cat -bu"
export RELEASE=`freebsd-version -ku | sort -r |head -1`-update
umask 027
cd /etc
test -d .git || git init .
git add -A
git commit --allow-empty -am ${RELEASE}
zfs snapshot -r zroot@${NOW}:${RELEASE}

download your txz files

  • base.txz
  • kernel.txz
  • src.txz and -dbg as required

create a new boot environment

Normally, you’d use bectl(8) to create a new boot environment, and it would do so using another boot environment to reference as a snapshot, so you’re not starting from an empty system. In our case, we absolutely do not want to inherit all the garbage from the other BE, so we will make a new zfs dataset, with the correct mount-related settings, and have it beautifully pristine and empty:

zfs create -o canmount=noauto -o mountpoint=/ zroot/ROOT/vanilla
bectl mount vanilla /mnt

The key settings here are:

  • canmount=noauto as the FreeBSD loader will mount only the active BE at startup. If you don’t set this, then zfs will, once you’re past the initial boot loader stage, happily load your different kernel/userland from a different boot environment over the top of your expected one. This is extremely frustrating to realise, and then fix!
  • mountpoint=/ clearly the root filesystem needs to be mounted at / and we must not inherit some other mountpoint from elsewhere

unpack your tarballs

This is part of the dirty work - we need to exclude some of /etc as we’re definitely wanting to keep our existing users and passwords, but the rest of the /etc dir such as /etc/rc.d we definitely want the side-loaded version to be present.

So we exclude the critical bits, and then assume that our existing setup’s files are compatible. This is usually true, but not always, and if you’re reading this, it’s reasonable to expect you’re willing to clean your own mess up when something breaks - it’s always just a boot environment away!

# cd /path/to/your/stash
tar xvzpf ./kernel-dbg.txz -C /mnt/
tar xvzpf ./kernel.txz -C /mnt/
tar xvzpf ./base-dbg.txz -C /mnt/
tar xvzpf ./base.txz -C /mnt/ \
  --clear-nochange-fflags \
  --exclude crontab \
  --exclude dhclient.conf \
  --exclude group \
  --exclude pwd.db \
  --exclude spwd.db \
  --exclude hosts \
  --exclude master.passwd \
  --exclude passwd \
  --exclude shells \
  --exclude sshd_config \
  --exclude sysctl.conf
zfs snapshot -r zroot/ROOT/vanilla@tarballs

move in /etc and friends

Anything in /etc and anything that is a softlink in /etc needs to move:

mv /mnt/etc /mnt/etc.dist
cp -av /etc /mnt/
cp -av /usr/local/etc /mnt/usr/local/
cp -av /boot/loader.conf* /mnt/boot/
cp -av /var/unbound /mnt/var/
zfs snapshot -r zroot/ROOT/vanilla@configs

tidy up our dirty work

This is typically where things go wrong - missing mountpoints ensure that a system will not get past single-user mode, any video/storage/network drivers that are required at loader stage will stop earlier. Confirm you have console access!

This is the really dirty spot. In particular:

  • the password file schema & default users & groups, could be different between FreeBSD versions
  • we need to make sure all mountpoints are available inside the chroot; my dirty hack might not be sufficient for your needs
  • the /etc dir we side-loaded is very likely to be different in key aspects between versions. You may need to polish, this is where the git repo of /etc comes in very handy
cd /mnt/etc
umask 027
git status
### make poor life choices now and edit away
git checkout -- want_other_version_of_this_file
git add -A
git commit -am 'post-13 sideload tweaks'

Now that we’ve done terrible things, we drop back into the chroot and rebuild our password file for the side-loaded FreeBSD version:

chroot /mnt /bin/sh
mkdir -p $(egrep -v 'none|^#' /etc/fstab | cut -wf 2 | sort | uniq)
uname -vm
    FreeBSD 14.0-CURRENT main-n244978-e1b88764b9c4 GENERIC  amd64
freebsd-version -kru
    13.0-BETA3    <---- our installed userland in the chroot
    14.0-CURRENT  <---- actual running kernel
    13.0-BETA3    <---- our installed kernel in the chroot
ln -s /usr/home /home
pwd_mkdb /etc/master.passwd
exit

Note that the new version 13.0-BETA3 is already showing up, even though we’ve not rebooted. This makes sense once you know how FreeBSD determines what version you’re running: freebsd-version uses on-disk information (from installed files) to determine what the installed kernel is, and that obviously can be different to the actual running version, during upgrades and boot environments.

install packages

From outside the chroot, run a final snapshot, and then we can install packages. Note that /etc/resolv.conf in the chroot must be usable!

At this point, we also need devfs and tmpfs to keep our chroot nice and tidy. If you don’t have a GB of ram free for a tmpfs, then consider nullfs mounting. Keeping the new BE clean is worth it.

zfs snapshot -r zroot/ROOT/vanilla@tidy`
mount -t devfs devfs /mnt/dev
mount -t tmpfs tmpfs /mnt/tmp
mount -t tmpfs tmpfs /mnt/var/cache/pkg
chroot /mnt /bin/sh
pkg install -r FreeBSD `cat /etc/packages.list`
exit
### outside the chroot again
zfs snapshot -r zroot/ROOT/vanilla@packages`

update boot blocks and/or EFI partitions

The final step is to ensure your boot blocks (UEFI or MBR) match. I’m leaving that as “exercise to the reader” but feel free to email me with any updates you figured out. Make sure to do this on all disks if you have zfs mirrors, or more than one disk.

activate and reboot

bectl activate vanilla
reboot

thanks

Guntbert Reiter replied with questions, and thus helped improve the article. Thanks for reading!