User:Halscode/Atomic Alpine: Difference between revisions
mNo edit summary |
No edit summary |
||
Line 420: | Line 420: | ||
# vi /usr/sbin/sysmut</pre> | # vi /usr/sbin/sysmut</pre> | ||
Example script to mutate | Example script to mutate the system: | ||
<pre>#!/bin/execlineb -W | <pre>#!/bin/execlineb -W | ||
Line 441: | Line 441: | ||
if { mkdir -p ${mnt}/snapshots/${newsnap} } | if { mkdir -p ${mnt}/snapshots/${newsnap} } | ||
if { btrfs subvolume snapshot ${mnt}/current/${source}/@ ${mnt}/snapshots/${newsnap}/@ } | if { btrfs subvolume snapshot ${mnt}/current/${source}/@ ${mnt}/snapshots/${newsnap}/@ } | ||
if { btrfs subvolume snapshot ${mnt}/current/${source}/@etc ${mnt}/snapshots/${newsnap}/@etc } | |||
if { | if { | ||
redirfd -w 1 ${mnt}/snapshots/${newsnap}/@ | redirfd -w 1 ${mnt}/snapshots/${newsnap}/@etc/fstab | ||
sed s#CURRENT_SNAPSHOTS_PATH#/snapshots/${newsnap}#g ${mnt}/fstab | sed s#CURRENT_SNAPSHOTS_PATH#/snapshots/${newsnap}#g ${mnt}/fstab | ||
} | } | ||
Line 449: | Line 450: | ||
if { mount -o bind,ro /dev ${mnt}/snapshots/${newsnap}/@/dev } | if { mount -o bind,ro /dev ${mnt}/snapshots/${newsnap}/@/dev } | ||
foreground { | foreground { | ||
foreground { mount -o bind,ro /etc/resolv.conf ${mnt}/snapshots/${newsnap}/@ | foreground { mount -o bind,ro /etc/resolv.conf ${mnt}/snapshots/${newsnap}/@etc/resolv.conf } | ||
foreground { | foreground { | ||
chroot ${mnt}/snapshots/${newsnap}/@ | chroot ${mnt}/snapshots/${newsnap}/@ | ||
foreground { mount -a } | foreground { mount -a } | ||
foreground { echo "=== Entering sysmut (make your changes now)" } | |||
foreground { sh } | foreground { sh } | ||
importas apply ? | importas apply ? | ||
Line 459: | Line 461: | ||
} | } | ||
importas apply ? | importas apply ? | ||
foreground { redirfd -w 2 /dev/null umount ${mnt}/snapshots/${newsnap}/@ | foreground { redirfd -w 2 /dev/null umount ${mnt}/snapshots/${newsnap}/@etc/resolv.conf } | ||
ifelse { exit ${apply} } { | ifelse { exit ${apply} } { | ||
if { btrfs property set -ts ${mnt}/snapshots/${newsnap}/@ ro true } | if { btrfs property set -ts ${mnt}/snapshots/${newsnap}/@ ro true } | ||
Line 471: | Line 473: | ||
if { ln -sfn ./links/${newlink} ${mnt}/next/current } | if { ln -sfn ./links/${newlink} ${mnt}/next/current } | ||
if { mv ${mnt}/next/current ${mnt}/ } | if { mv ${mnt}/next/current ${mnt}/ } | ||
echo "Changes applied" | echo "=== Changes applied" | ||
} | } | ||
echo "Changes discarded" | # Remove failed snapshot | ||
btrfs subvolume delete ${mnt}/snapshots/${newsnap}/@ | |||
btrfs subvolume delete ${mnt}/snapshots/${newsnap}/@etc | |||
rm -r ${mnt}/snapshots/${newsnap} | |||
echo "=== Changes discarded" | |||
} | } | ||
foreground { redirfd -w 2 /dev/null umount ${mnt}/snapshots/${newsnap}/@/proc } | foreground { redirfd -w 2 /dev/null umount ${mnt}/snapshots/${newsnap}/@/proc } |
Latest revision as of 07:14, 2 October 2024
Specifically, this is a draft I (Halscode) am working on as I build my own system based on the linked guide. See Talk:Immutable root with atomic upgrades to see the changes I made to get the guide to work.
What?
This article provides a basic guide to setting up a read-only-root-based Alpine Linux system with several boot environments and atomic upgrades using a modern bootloader and btrfs.
Why?
Read-only root and atomic upgrades with the ability to easily rollback or boot previous configurations is a concept that has been gaining popularity recently. Distributions providing and promoting such features, for example, are Fedora Silverblue, Opensuse MicroOS, NixOS and GNU Guix.
While Alpine Linux has its killer features, it lacks the ones mentioned above on default setup. This is a proof of concept that it's possible to implement them in a minimal way on a minimal system.
Preparation
You should have bootable Alpine media. The process to obtain it is described on the installation page.
If you cannot connect to the Internet via Ethernet, you will need to install and set up wpa_supplicant to connect to the Internet. Follow the linked instructions until the header "Automatic Configuration on System Boot"; since the installer system is not permanent, that step is not necessary. The standard image does not include iwd, so you must use wpa_supplicant for the setup process.
The installer will need several packages that are not available on the base images. Set up your network and repository:
# setup-interfaces # if you've already set up wifi, you can skip this # setup-apkrepos
Partitioning disks
In this guide, it's assumed that you have a fresh UEFI system without an OS and have just booted into a live Alpine system using a USB flash drive or CD.
If you aren't planning to re-use an existing partition, create a new, clean partition table on your target storage device (we'll use /dev/sda
):
# apk add gptfdisk # gdisk /dev/sda > o ↵ # creates a new GPT partition table > y ↵ # confirm > w ↵ # commits this new table to disk > y ↵ # confirm
Now we can define the partitions:
# cgdisk /dev/sda
Partition creation process consists of several steps:
- Start sector - you can safely use default value by pressing ↵
- Size
- Type (as hex code) - EFI is ef00, Linux filesystem is 8300, Swap is 8200.
Result table:
Part. # Size Partition Type Partition Name ---------------------------------------------------------------- 1 200.0 MiB EFI System EFI 2 200.0 GiB Linux filesystem ROOT 3 32.0 GiB Linux swap SWAP
ROOT
partition name will later be used in rEFInd configuration to identify boot volume.
-16G
. This gives it 16 GiB of space to work with, so the size should remain default (which will be 16GiB). The swap size should be between as much RAM as you have and double that.
If you don't use swap, you may encounter performance issues and you will not be able to suspend to disk.
You will likely need to manually re-read the partition table to get the block devices needed for the next step. To do this:
# partprobe /dev/sda # mdev -s
Next step is creating filesystems:
# apk add btrfs-progs # includes mkfs.btrfs # mkfs.vfat -F32 /dev/sda1 # EFI # mkfs.btrfs /dev/sda2 # ROOT # mkswap /dev/sda3 # SWAP
Now we can mount our root volume:
# mount -t btrfs /dev/sda2 /mnt
File system structure
Now we should create the file structure that would provide reliable atomic system upgrades.
Start with the following directories:
# mkdir /mnt/next
Stores next current
link, is necessary due to how busybox mv
does atomic link replacement.
# mkdir /mnt/commons
Stores common non-snapshotting subvolumes.
We may populate it right away:
# btrfs subvolume create /mnt/commons/@var # btrfs subvolume create /mnt/commons/@home
Let's go ahead and add a couple needed folders for /var
before they become an issue:
# mkdir /mnt/commons/@var/empty # mkdir /mnt/commons/@var/tmp # this will be mounted over
Next, the most important directories:
# mkdir /mnt/snapshots
Stores directories containing snapshots belonging to one generation.
# mkdir /mnt/links
Stores generations of directories containing links to snapshot generations.
Let's create the first generation and populate it with one OS root snapshot @
:
# NEWSNAPSHOTS="$(date -u +"%Y%m%d%H%M%S")$(cat /dev/urandom | tr -dc 'a-zA-Z' | fold -w 8 | head -n 1)" # mkdir "/mnt/snapshots/$NEWSNAPSHOTS" # btrfs subvolume create /mnt/snapshots/$NEWSNAPSHOTS/@ # btrfs subvolume create /mnt/snapshots/$NEWSNAPSHOTS/@etc
Populate links
:
# NEWLINKS="$(date -u +"%Y%m%d%H%M%S")$(cat /dev/urandom | tr -dc 'a-zA-Z' | fold -w 8 | head -n 1)" # mkdir "/mnt/links/$NEWLINKS" # ln -s "../../snapshots/$NEWSNAPSHOTS" "/mnt/links/$NEWLINKS/0" # ln -s "../../snapshots/$NEWSNAPSHOTS" "/mnt/links/$NEWLINKS/1" # ln -s "../../snapshots/$NEWSNAPSHOTS" "/mnt/links/$NEWLINKS/2" # ln -s "../../snapshots/$NEWSNAPSHOTS" "/mnt/links/$NEWLINKS/3"
You can have as many links as you like, just apply changes to rEFInd config and upgrade scripts described below accordingly.
Link that will point to latest links generation:
# ln -s "./links/$NEWLINKS" /mnt/current
This setup allows us to just have static rEFInd config that points to to /current/0/@
, /current/1/@
, etc. while the actual underlying boot environment will change with each upgrade.
But how will fs mounting services know which snapshot generation is currently loaded?
The answer is common fstab
in the btrfs root.
Get UUIDs of the partitions first (this puts them in the file for easy access):
# blkid > /mnt/fstab
Now edit fstab accordingly:
# vi /mnt/fstab
Replace the UUIDs below, but it should look like:
UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 / btrfs subvol=CURRENT_SNAPSHOTS_PATH/@,ro,noatime 0 0 UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 /etc btrfs subvol=CURRENT_SNAPSHOTS_PATH/@etc,rw,noatime 0 0 UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 /var btrfs subvol=/commons/@var,rw,noatime 0 0 UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 /home btrfs subvol=/commons/@home,rw,noatime 0 0 # UUID=2FE6-837A /boot/efi vfat rw,noatime,discard 0 2 tmpfs /tmp tmpfs mode=1777,noatime,nosuid,nodev,size=2G 0 0 tmpfs /var/tmp tmpfs mode=1777,noatime,nosuid,nodev,size=2G 0 0 UUID=f0239163-9d46-47c1-67a4-3ee1d63d0676 swap swap rw,noatime,discard 0 0
CURRENT_SNAPSHOTS_PATH
will be replaced by sysmut with, for example, /snapshots/20210411212549sdBXyLxg
, and the result will be piped into /etc/fstab
of a created snapshot while applying it.
mount -t btrfs /dev/sda2 /mnt
) and edit /mnt/fstab
. You do not have to do this within sysmut, but it is recommended to do it that way, since your changes will then be applied for the next boot when you exit.Here's what the root btrfs volume structure mounted on /mnt
should look like now:
|--mnt | |--commons | | |--@var | | |--@home | |--current | |--fstab | |--links | | |--20210411213742qwrXAJBz | | | |--0 | | | |--1 | | | |--2 | | | |--3 | |--next | |--snapshots | | |--20210411212549sdBXyLxg | | | |--@ | | | |--@etc
Base system install
With the directory structure prepared, we can begin installing a basic Alpine Linux system.
Considering that installation is done from Alpine system, we only need following parts of the process:
# apk -X https://dl-cdn.alpinelinux.org/alpine/latest-stable/main -U --allow-untrusted -p /mnt/snapshots/20210411212549sdBXyLxg/@ --initdb add alpine-base
Copy the contents of /etc
to the etc subvolume:
# cp -r /mnt/snapshots/20210411212549sdBXyLxg/@/etc/* /mnt/snapshots/20210411212549sdBXyLxg/@etc
Now we can setup basic chroot to complete the installation process:
# export SNP="/mnt/snapshots/20210411212549sdBXyLxg/@" # # tip: use tab to fill in the correct snapshot; this one is just an example # mount -o bind /dev $SNP/dev # mount -t proc none $SNP/proc # mount -t sysfs sys $SNP/sys # sed "s#CURRENT_SNAPSHOTS_PATH#/snapshots/20210411212549sdBXyLxg#g" /mnt/fstab > "$SNP/etc/fstab" # cp -L /etc/resolv.conf "$SNP/etc/" # so DNS still works # chroot "$SNP" /bin/sh # mount -a
As soon as you're in chroot, define repositories:
# echo "https://dl-cdn.alpinelinux.org/alpine/latest-stable/main" >> /etc/apk/repositories
This example shows only main
, but you should also add testing
and community
if you need any packages in those. You can do this by repeating that command, but with testing
or community
instead of main
accordingly.
Now it's time for the firmware, kernel, and btrfs packages:
# setup-apkrepos # apk add -U linux-firmware linux-lts btrfs-progs
linux-firmware
to a custom set of firmware packages suitable for your system, for example, linux-firmware-amd linux-firmware-amd-ucode linux-firmware-amdgpu linux-firmware-ath10k linux-firmware-qca
for a typical AMD laptop.It's also important to add the btrfs
feature to mkinitfs.conf
and run mkinitfs
manually:
# vi /etc/mkinitfs/mkinitfs.conf # mkinitfs "$(ls /lib/modules)"
These steps prepare the kernel and generate the initramfs
, which will be used later to boot from our first snapshot.
Now, you should add the sysmut and syscln, scripts, which you'll use to upgrade the system, add new software if you need, and clean up old snapshots.
This would be a good time to:
- install and set up doas
- run setup-desktop
- install NetworkManager if you installed GNOME with setup-desktop (and if you need WiFi, networkmanager-wifi as well)
- Make sure you install execline so you can run sysmut and syscln!
- install wpa_supplicant or iwd to use WiFi
- install alternate shells, such as zsh, bash, and/or fish (and
chsh
them as you wish)
iwd
or wpa_supplicant
, so you will not end up severed from network on your first boot. The network connection from the installer does not carry over onto the installed system! So if you need WiFi and you don't do this, you may not be able to get it back!
Configure the system
Start with setting a password for the root:
# passwd root
Don't forget to add essential services to their respective runlevels:
rc-update add devfs sysinit rc-update add dmesg sysinit rc-update add mdev sysinit rc-update add hwdrivers sysinit rc-update add hwclock boot rc-update add modules boot rc-update add sysctl boot rc-update add hostname boot rc-update add bootmisc boot rc-update add syslog boot rc-update add mount-ro shutdown rc-update add killprocs shutdown rc-update add savecache shutdown
With the snapshot prepared and configured, we can chroot out of it and unmount everything:
# umount -a # exit
Finish editing the snapshot by setting the ro
flag and unmounting the root volume:
# btrfs property set -ts "/mnt/snapshots/20210411212549sdBXyLxg/@" ro true # umount /mnt
Bootloader installation
Mount the EFI partition:
# mount -t vfat /dev/sda1 /mnt # mkdir /mnt/EFI
Bootloader configuration
There are 2 options as examples of the bootloader installation: rEFInd
and GRUB
.
Sometimes one of them will refuse to work on a system for no particular reason, in this case try the other one.
rEFInd
Check the latest version number of the refind
package:
# echo "https://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories # apk update # apk info refind
Manually download the latest version (replace 0.13.2-r3 in the example below) of the refind
package:
# wget https://dl-cdn.alpinelinux.org/alpine/edge/testing/x86_64/refind-0.13.2-r3.apk
Unpack the prepared rEFInd archive and copy relevant files to /mnt/EFI/
# tar -xzf refind-0.13.2-r3.apk # cp -r usr/share/refind /mnt/EFI/ # cd /mnt/EFI/refind
Rename config file and edit it:
# mv refind.conf-sample refind.conf # vi refind.conf
And append following to the end of the file, remember to replace example UUIDs with your own for root
(btrfs partition) and resume
(swap partition). Keep in mind that if you named the btrfs volume other than ROOT
during "Partitioning disks" stage, you have to change the volume
field below accordingly.
# ONLY include the below line on x86_64 systems: this works around a bug in rEFInd scan_driver_dirs EFI/refind/drivers_x86_64 menuentry "Alpine Linux" { icon /EFI/refind/icons/os_linux.png volume "ROOT" loader /current/0/@/boot/vmlinuz-lts initrd /current/0/@/boot/initramfs-lts options "root=UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 rootfstype=btrfs rootflags=subvol=/current/0/@,ro,noatime resume=UUID=f0239163-9d46-47c1-67a4-3ee1d63d0676 quiet splash" submenuentry "Boot fallback 1" { loader /current/1/@/boot/vmlinuz-lts initrd /current/1/@/boot/initramfs-lts options "root=UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 rootfstype=btrfs rootflags=subvol=/current/1/@,ro,noatime resume=UUID=f0239163-9d46-47c1-67a4-3ee1d63d0676 quiet splash" } submenuentry "Boot fallback 2" { loader /current/2/@/boot/vmlinuz-lts initrd /current/2/@/boot/initramfs-lts options "root=UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 rootfstype=btrfs rootflags=subvol=/current/2/@,ro,noatime resume=UUID=f0239163-9d46-47c1-67a4-3ee1d63d0676 quiet splash" } submenuentry "Boot fallback 3" { loader /current/3/@/boot/vmlinuz-lts initrd /current/3/@/boot/initramfs-lts options "root=UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 rootfstype=btrfs rootflags=subvol=/current/3/@,ro,noatime resume=UUID=f0239163-9d46-47c1-67a4-3ee1d63d0676 quiet splash" } }
"ROOT"
is the PARTLABEL
of the btrfs partition. You may also use PARTUUID
instead. To get both blkid
from blkid package can be used. blkid
included in busybox does not provide this information.You cannot use labels in
options
. You'll need to replace it with the partition's UUID.All you have to do now is reboot, remove your media, and you should be done:
# umount /mnt
# reboot
Using the overlaytmpfs
option
You can use overlayfs with tmpfs built into Alpine's init script to allow changes in the rootfs which will be automatically reverted upon reboot.
To make use of this, add overlaytmpfs
to the kernel boot options in refind.conf
, e.g.:
... initrd /current/0/@/boot/initramfs-lts options "root=UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 rootfstype=btrfs rootflags=subvol=/current/0/@,ro,noatime resume=UUID=f0239163-9d46-47c1-67a4-3ee1d63d0676 overlaytmpfs quiet splash" submenuentry "Boot fallback 1" { ...
You may want to do this to each submenuentry as well, or instead create separate submenuentries for each snapshot. So instead, the file might look like:
# ONLY include the below line on x86_64 systems: this works around a bug in rEFInd scan_driver_dirs EFI/refind/drivers_x86_64 menuentry "Alpine Linux" { icon /EFI/refind/icons/os_linux.png volume "ROOT" loader /current/0/@/boot/vmlinuz-lts initrd /current/0/@/boot/initramfs-lts options "root=UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 rootfstype=btrfs rootflags=subvol=/current/0/@,ro,noatime resume=UUID=f0239163-9d46-47c1-67a4-3ee1d63d0676 quiet splash" submenuentry "Boot fallback 1" { loader /current/1/@/boot/vmlinuz-lts initrd /current/1/@/boot/initramfs-lts options "root=UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 rootfstype=btrfs rootflags=subvol=/current/1/@,ro,noatime resume=UUID=f0239163-9d46-47c1-67a4-3ee1d63d0676 quiet splash" } submenuentry "Boot fallback 2" { loader /current/2/@/boot/vmlinuz-lts initrd /current/2/@/boot/initramfs-lts options "root=UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 rootfstype=btrfs rootflags=subvol=/current/2/@,ro,noatime resume=UUID=f0239163-9d46-47c1-67a4-3ee1d63d0676 quiet splash" } submenuentry "Boot fallback 3" { loader /current/3/@/boot/vmlinuz-lts initrd /current/3/@/boot/initramfs-lts options "root=UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 rootfstype=btrfs rootflags=subvol=/current/3/@,ro,noatime resume=UUID=f0239163-9d46-47c1-67a4-3ee1d63d0676 quiet splash" } submenuentry "Boot current with overlaytmpfs" { loader /current/0/@/boot/vmlinuz-lts initrd /current/0/@/boot/initramfs-lts options "root=UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 rootfstype=btrfs rootflags=subvol=/current/0/@,ro,noatime resume=UUID=f0239163-9d46-47c1-67a4-3ee1d63d0676 overlaytmpfs quiet splash" } submenuentry "Boot fallback 1 with overlaytmpfs" { loader /current/1/@/boot/vmlinuz-lts initrd /current/1/@/boot/initramfs-lts options "root=UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 rootfstype=btrfs rootflags=subvol=/current/1/@,ro,noatime resume=UUID=f0239163-9d46-47c1-67a4-3ee1d63d0676 overlaytmpfs quiet splash" } submenuentry "Boot fallback 2 with overlaytmpfs" { loader /current/2/@/boot/vmlinuz-lts initrd /current/2/@/boot/initramfs-lts options "root=UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 rootfstype=btrfs rootflags=subvol=/current/2/@,ro,noatime resume=UUID=f0239163-9d46-47c1-67a4-3ee1d63d0676 overlaytmpfs quiet splash" } submenuentry "Boot fallback 3 with overlaytmpfs" { loader /current/3/@/boot/vmlinuz-lts initrd /current/3/@/boot/initramfs-lts options "root=UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 rootfstype=btrfs rootflags=subvol=/current/3/@,ro,noatime resume=UUID=f0239163-9d46-47c1-67a4-3ee1d63d0676 overlaytmpfs quiet splash" } }
GRUB
# apk add grub-efi
GRUB requires two configuration files this time as we will use grub-mkstandalone
.
The first configuration file is internal and should only point to the second file, where we store the menu:
# cd /tmp # vi grub_internal.cfg
Set the contents to the following, but make sure to replace 2FE6-837A
with your own EFI partition UUID:
insmod part_gpt insmod fat search --set efi --fs-uuid 2FE6-837A configfile (${efi})/EFI/grub/grub.cfg
The second config file is the main config where we describe the entire boot menu.
# vi grub.cfg
Set to contain, but replace UUIDs:
set timeout=3 menuentry "Alpine Linux Current" { search --set root --fs-uuid b9ff5e7b-e128-4e64-861a-2fdd794a9828 linux /current/0/@/boot/vmlinuz-edge root=UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 rootfstype=btrfs rootflags=subvol=/current/0/@,ro,noatime resume=UUID=f0239163-9d46-47c1-67a4-3ee1d63d0676 quiet splash initrd /current/0/@/boot/initramfs-edge } menuentry "Alpine Linux Snapshot 1" { search --set root --fs-uuid b9ff5e7b-e128-4e64-861a-2fdd794a9828 linux /current/1/@/boot/vmlinuz-edge root=UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 rootfstype=btrfs rootflags=subvol=/current/1/@,ro,noatime resume=UUID=f0239163-9d46-47c1-67a4-3ee1d63d0676 quiet splash initrd /current/1/@/boot/initramfs-edge } menuentry "Alpine Linux Snapshot 2" { search --set root --fs-uuid b9ff5e7b-e128-4e64-861a-2fdd794a9828 linux /current/2/@/boot/vmlinuz-edge root=UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 rootfstype=btrfs rootflags=subvol=/current/2/@,ro,noatime resume=UUID=f0239163-9d46-47c1-67a4-3ee1d63d0676 quiet splash initrd /current/2/@/boot/initramfs-edge } menuentry "Alpine Linux Snapshot 3" { search --set root --fs-uuid b9ff5e7b-e128-4e64-861a-2fdd794a9828 linux /current/3/@/boot/vmlinuz-edge root=UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 rootfstype=btrfs rootflags=subvol=/current/3/@,ro,noatime resume=UUID=f0239163-9d46-47c1-67a4-3ee1d63d0676 quiet splash initrd /current/3/@/boot/initramfs-edge }
Generate the grubx64.efi
binary:
# grub-mkstandalone -O x86_64-efi -o grubx64.efi "boot/grub/grub.cfg=/tmp/grub_internal.cfg" # mkdir /mnt/EFI/grub # mv grubx64.efi /mnt/EFI/grub/ # mv grub.cfg /mnt/EFI/grub/
Adding EFI boot entry
To add the chosen bootloader to UEFI, efibootmgr
is a suitable tool. The following example is for rEFInd, but could be easily adjusted for GRUB:
# apk add efibootmgr # efibootmgr --create --disk /dev/sda --part 1 --loader /EFI/refind/refind_x64.efi --label "rEFInd" --verbose
/dev/sda
is our disk device and 1
is the number of the FAT32 partition containing the bootloader data.
Updating or altering the system
execline
and require the execline
package in the system.
execline
provides a number of runtime advantages and the resulting script is much more readable.# touch /usr/sbin/sysmut # chmod +x /usr/sbin/sysmut # vi /usr/sbin/sysmut
Example script to mutate the system:
#!/bin/execlineb -W unshare --mount importas -D 0 source 1 define mnt /media/root if { mkdir -p ${mnt} } if { mount -t btrfs -o rw,noatime UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 ${mnt} } foreground { backtick -E dt { date -u +%Y%m%d%H%M%S } backtick -E rnd { pipeline { cat /dev/urandom } pipeline { tr -dc a-zA-Z } pipeline { fold -w 8 } head -n 1 } define newsnap ${dt}${rnd} if { mkdir -p ${mnt}/snapshots/${newsnap} } if { btrfs subvolume snapshot ${mnt}/current/${source}/@ ${mnt}/snapshots/${newsnap}/@ } if { btrfs subvolume snapshot ${mnt}/current/${source}/@etc ${mnt}/snapshots/${newsnap}/@etc } if { redirfd -w 1 ${mnt}/snapshots/${newsnap}/@etc/fstab sed s#CURRENT_SNAPSHOTS_PATH#/snapshots/${newsnap}#g ${mnt}/fstab } if { mount -t proc none ${mnt}/snapshots/${newsnap}/@/proc } if { mount -t sysfs sys ${mnt}/snapshots/${newsnap}/@/sys } if { mount -o bind,ro /dev ${mnt}/snapshots/${newsnap}/@/dev } foreground { foreground { mount -o bind,ro /etc/resolv.conf ${mnt}/snapshots/${newsnap}/@etc/resolv.conf } foreground { chroot ${mnt}/snapshots/${newsnap}/@ foreground { mount -a } foreground { echo "=== Entering sysmut (make your changes now)" } foreground { sh } importas apply ? foreground { umount -a } exit ${apply} } importas apply ? foreground { redirfd -w 2 /dev/null umount ${mnt}/snapshots/${newsnap}/@etc/resolv.conf } ifelse { exit ${apply} } { if { btrfs property set -ts ${mnt}/snapshots/${newsnap}/@ ro true } define newlink ${dt}${rnd} if { mkdir -p ${mnt}/links/${newlink} } if { ln -s ../../snapshots/${newsnap} ${mnt}/links/${newlink}/0 } if { cp -P ${mnt}/current/0 ${mnt}/links/${newlink}/1 } if { cp -P ${mnt}/current/1 ${mnt}/links/${newlink}/2 } if { cp -P ${mnt}/current/2 ${mnt}/links/${newlink}/3 } if { mkdir -p ${mnt}/next } if { ln -sfn ./links/${newlink} ${mnt}/next/current } if { mv ${mnt}/next/current ${mnt}/ } echo "=== Changes applied" } # Remove failed snapshot btrfs subvolume delete ${mnt}/snapshots/${newsnap}/@ btrfs subvolume delete ${mnt}/snapshots/${newsnap}/@etc rm -r ${mnt}/snapshots/${newsnap} echo "=== Changes discarded" } foreground { redirfd -w 2 /dev/null umount ${mnt}/snapshots/${newsnap}/@/proc } foreground { redirfd -w 2 /dev/null umount ${mnt}/snapshots/${newsnap}/@/sys } redirfd -w 2 /dev/null umount ${mnt}/snapshots/${newsnap}/@/dev } umount ${mnt}
It will get you into the root shell chrooted into the new snapshot, where you can apply any change you like. The origin of the new snapshot is defined by the first and only argument, in form of number. If no argument provided the 0
(current latest) is taken as origin.
If chroot shell exits with an error, there will be no switch to the new snapshots. This means you can manually discard changes while in the chroot by:
# exit 1
Deleting unused snapshots
Unused snapshots can be garbage-collected by:
# touch /usr/sbin/syscln # chmod +x /usr/sbin/syscln # vi /usr/sbin/syscln
#!/bin/execlineb -W unshare --mount define mnt /media/root if { mkdir -p ${mnt} } if { mount -t btrfs -o rw,noatime,compress=zstd:3 UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 ${mnt} } foreground { foreground { pipeline { foreground { pipeline { find -H ${mnt}/snapshots/ -maxdepth 1 -mindepth 1 -print0 } xargs -0 -r realpath } pipeline { find -H ${mnt}/current/ -maxdepth 1 -mindepth 1 -print0 } xargs -0 -r realpath } pipeline { tr \\n \\0 } pipeline { sort -z } pipeline { uniq -u -z } pipeline { xargs -0 -r -n 1 -I [] find -H [] -maxdepth 1 -mindepth 1 -print0 } xargs -0 -r btrfs subvolume delete } foreground { find -H ${mnt}/snapshots/ -maxdepth 1 -mindepth 1 -empty -type d -delete } foreground { pipeline { foreground { pipeline { find -H ${mnt}/links/ -maxdepth 1 -mindepth 1 -print0 } xargs -0 -r realpath } realpath ${mnt}/current } pipeline { tr \\n \\0 } pipeline { sort -z } pipeline { uniq -u -z } pipeline { xargs -0 -r -n 1 -I [] find -H [] -maxdepth 1 -mindepth 1 -print0 } xargs -0 -r -n 1 unlink } find -H ${mnt}/links/ -maxdepth 1 -mindepth 1 -empty -type d -delete } umount ${mnt}