Merge pull request '3-xen-orchestra-install' (#4) from 3-xen-orchestra-install into main

Reviewed-on: #4
This commit is contained in:
hygienic-books 2023-10-22 14:23:53 +00:00
commit 38396d7ccf
3 changed files with 760 additions and 214 deletions

260
README.md
View File

@ -8,8 +8,37 @@ We expect minimal prep on your end. Please make sure that before execution the f
- Arch Linux live CD ISO image sees exactly one partition with partition type code `BF00` ("Solaris root")
- Arch Linux live CD ISO image sees exactly one partition with partition type code `EF00` ("EFI system partition")
- The `EF00` EFI partition is mountable, in practical terms this usually only means it has a file system.
- No ZFS zpool exists
### How to prep
On a blank example disk `/dev/sda` you can fulfill the requirements (One `EF00` partition with a file system plus one `BF00` partition) for example like so:
```
sgdisk --new '1::+512M' --new '2' --typecode '1:EF00' --typecode '2:BF00' /dev/sda
mkfs.vfat /dev/sda1
```
> `--new '1::+512M'`: Create partition number `1`. The field separator `:` separates the partition number from start sector. In this case start sector is unspecified so start sector sits at whatever the system's default is for this operation. On a blank disk on an Arch Linux live CD ISO image this will default to sector `2048`. Partition ends at whatever the beginning is `+512M` meaning plus 512 Mebibytes.
>
> `--new '2'`: Create partition number `2`. Both field number 2, the start sector, and field number 3, the end sector, are unspecified, there's no field separator `:`. Field number 2 will be the first free sector - in this case right after partition 1 - and field number 3 will be end of disk. Thus partition `2` will fill the remaining free disk space.
>
> `--typecode '1:EF00'`: Partition 1 gets partition type code `EF00`, an EFI system partition.
>
> `--typecode '2:BF00'`: Partition 2 gets partition type code `BF00`, a Solaris root partition.
The result will be something like this at which point you can start the `setup.sh` script, see [How to run this?](#how-to-run-this) below for more details.
```
# lsblk --paths
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
/dev/loop0 7:0 0 685.5M 1 loop /run/archiso/airootfs
/dev/sr0 11:0 1 808.3M 0 rom /run/archiso/bootmnt
/dev/sda 202:0 0 10G 0 disk
├─/dev/sda1 202:1 0 512M 0 part
└─/dev/sda2 202:2 0 9.5G 0 part
```
## ZFS dataset layout
The script will create a single ZFS zpool `zpool` on the `BF00` partition with dataset child `zpool/root` which itself has one child `zpool/root/archlinux`, that's where Arch Linux gets installed. Parallel to `zpool/root` it'll create `zpool/data` with a `zpool/data/home` child dataset that gets mounted at `/home`.
The script will use the `EF00` partition to install a ZFSBootMenu EFI executable if `efibootmgr` says that no such `ZFSBootMenu` entry exists. If ZFSBootMenu gets added to the EFI partition it'll become primary boot option.
@ -19,17 +48,37 @@ The script will use the `EF00` partition to install a ZFSBootMenu EFI executable
- Boot an Arch Linux live CD ISO image
- Run:
```
export SCRIPT_URL='https://quico.space/quico-os-setup/arch-zbm/raw/branch/main/setup.sh'
curl -s "${SCRIPT_URL}" | bash
export SCRIPT_URL='https://quico.space/quico-os-setup/arch-zbm/raw/branch/main/setup.sh' && curl -s "${SCRIPT_URL}" | bash
```
During execution the script will call itself when it changes into its `chroot`, that's why we `export SCRIPT_URL`. Feel free to update `"${SCRIPT_URL}"` with whatever branch or revision you want to use from [quico.space/quico-os-setup/arch-zbm](https://quico.space/quico-os-setup/arch-zbm). Typically `.../branch/main/setup.sh` as shown above is what you want.
### Options
#### Compression
By default we create a zpool with ZFS property `compression=on`. If the `lz4_compress` pool feature is active this will by default enable `compression=lz4`. See `man 7 zfsprops` for example in ZFS 2.1.9 for details. See `zpool get feature@lz4_compress <pool>` to check this feature's status on your `<pool>`.
To get a zpool with uncompressed datasets export the shell variable `ARCHZBM_ZFSPROPS_NO_COMPRESSION` with any value prior to running this script. Literally any value works as long as you're not setting this to an empty string:
```
export ARCHZBM_ZFSPROPS_NO_COMPRESSION=yesplease
```
#### Encryption
By default we encrypt the zpool with ZFS property `encryption=on`. In ZFS 2.1.9 this defaults to `encryption=aes-256-gcm`.
To get a zpool with unencrypted datasets export the shell variable `ARCHZBM_ZFSPROPS_NO_ENCRYPTION` with any value prior to running this script:
```
export ARCHZBM_ZFSPROPS_NO_ENCRYPTION=yup
```
## Steps
The scripts takes the following installation steps.
The script takes the following installation steps.
1. Install ZFS tools and kernel module with [github.com/eoli3n/archiso-zfs](https://github.com/eoli3n/archiso-zfs)
1. Create one encrypted ZFS zpool on top of `BF00` partition, password `password`
1. Create one ZFS zpool on top of `BF00` partition, encrypted and compressed datasets, password `password`
1. _See paragraphs [Compression](#compression)/[Encryption](#encryption) to optionally disable properties_
1. Create dataset for Arch Linux and `/home`
1. Install Arch Linux into pool
1. Add ZFSBootMenu to `EF00` partition if it doesn't exist already
@ -59,7 +108,37 @@ After installation you're going to want to at least touch these points in your n
- Passwords
- ZFS: The password for all datasets underneath `zpool` is `password`.
- Local `root` account: The local `root` account's password is `password`.
- Arch User Repository (AUR) helper: We installed [paru](https://github.com/Morganamilo/paru) as our AUR helper, we installed from GitHub via `makepkg -si`.
- Arch User Repository (AUR) helper: We installed [paru](https://github.com/Morganamilo/paru) as our AUR helper, we installed from GitHub via `makepkg -si` then replaced itself with its [paru-bin](https://aur.archlinux.org/packages/paru-bin) version from AUR.
- In `/etc/systemd/network/50-wired.network` instead of a DHCP-based network config you can get a static one. The DHCP-based one for reference looks like:
```
...
[Network]
DHCP=ipv4
IPForward=yes
Domains=~.
[DHCP]
UseDNS=yes
RouteMetric=10
```
A static config does away with the `[DHCP]` section:
```
...
[Network]
Address=10.10.10.2/24
Gateway=10.10.10.1
DNS=10.10.10.1
IPForward=yes
Domains=~.
```
- In case you later want a graphical interface and specifically NetworkManager (via package `networkmanager`) consider telling it to keep its hands off of some of your network interfaces. The bullet point above adds a `systemd`-style config file that `systemd-networkd.service` will read and use. Should you ever install NetworkManager it will by default assume that it must manage all interfaces. It'll use its own DHCP client to try and get IP addresses for _managed interfaces_ in which case you'll end up with whatever addressing scheme you configured in a `.network` unit file plus NetworkManager's additional address. Create `/etc/NetworkManager/conf.d/99-unmanaged-devices.conf` for example to declare some interfaces as off-limits or _unmanaged_:
```
[keyfile]
unmanaged-devices=mac:52:54:00:74:79:56;type:ethernet
```
Check out [ArchWiki article "NetworkManager" section "Ignore specific devices"](https://wiki.archlinux.org/title/NetworkManager#Ignore_specific_devices) for more info.
# Password change
@ -144,7 +223,7 @@ In order to generate a new master key after you've changed your user key as ment
- With `-v` we get verbose progress output
- Argument `-u` makes sure the dataset does not get mounted after transfer. ZFS would mount it into `/` which wouldn't be helpful since we're currently using that filesystem ourselves.
- We set encryption properties `keyformat`, `keylocation` and most importantly `encryption`. The latter will turn our transferred dataset into its own `encryptionroot` which in turn generates a new master key. The auto-generated new master key gets wrapped with our updated passphrase in `keylocation`. This basically reencrypts all data in this dataset during transfer.
- We set `mountpoint` and `canmount` as well as a `org.zfsbootmenu:commandline` as we would for any new system dataset.
- We set `mountpoint` and `canmount` as well as an `org.zfsbootmenu:commandline` as we would for any new system dataset.
1. Change zpool's `bootfs` property to new system dataset
```
zpool set bootfs=zpool/root/archlinux-frn zpool
@ -204,6 +283,8 @@ zpool status zpool
# ZFS setup explained
## Overview
The ZFS pool and dataset setup that makes this tick, explained in plain English.
1. Create zpool with options:
@ -215,8 +296,9 @@ The ZFS pool and dataset setup that makes this tick, explained in plain English.
1. `-O keylocation=file://...`: This property is only set for encrypted datasets which are encryption roots. Controls where the user's encryption key will be loaded from by default for commands such as `zfs load-key`.
1. `-O keyformat=passphrase`: Controls what format the user's encryption key will be provided as. Passphrases must be between 8 and 512 bytes long.
1. At this time the newly created zpool is not mounted anywhere. Next we create the "root" dataset, that's an arbitary term for the parent dataset of all boot environments. Boot environments in your case may be for example different operating systems all of which live on separate datasets underneath the root.
1. `-o canmount=off`: Same as above, the root dataset can - just like the pool - not be mounted.
1. `-o mountpoint=none`: Same as above, the root dataset has - just like the pool - no mountpoint configured.
1. `zfs set org.zfsbootmenu:commandline=...`: Set a common kernel command line for all boot environment such as `"ro quiet"`.
1. `zfs set org.zfsbootmenu:commandline=...`: Set a common kernel command line for all boot environments such as `"ro quiet"`.
1. Neither the root dataset nor the pool are mounted at this time. We now create one boot environment dataset where we want to install Arch Linux.
1. `-o mountpoint=/`: Our Arch Linux dataset will be mounted at `/`.
1. `-o canmount=noauto`: When set to `noauto`, a dataset can only be mounted and unmounted explicitly. The dataset is not mounted automatically when the dataset is created or imported, nor is it mounted by the `zfs mount -a` command or unmounted by the `zfs unmount -a` command.
@ -229,15 +311,171 @@ The ZFS pool and dataset setup that makes this tick, explained in plain English.
1. For a `zpool/data/home` child dataset:
1. We do not specify any properties. Since `canmount` cannot be inherited the parent's `canmount=off` does not apply, it instead defaults to `canmount=on`. The parent's `mountpoint=/` property on the other hand is inherited so for a `home` child dataset it conveniently equals `mountpoint=/home`.
1. In effect this `zpool/data/home` dataset is subject to `zfs mount -a` and will happily automount into `/home`.
1. We export the zpool once, we then reimport it by scanning only inside `/dev/disk/by-id`, again setting `-R /mnt` as we did during pool creation a moment ago and we do not mount any file systems.
1. We export the zpool once, we then reimport it by scanning only inside `/dev/disk/by-partuuid`, again setting `-R /mnt` as we did during pool creation a moment ago and we do not mount any file systems.
1. We `zfs load-key <encryptionroot>` which will load the key from `keylocation` after which the `keystatus` property for `<encryptionroot>` and all child datasets will change from `unavailable` to `available`.
1. We mount our Arch Linux boot environment dataset. It automatically get prepended with `-R /mnt` since that's how we imported the pool.
1. We mount our Arch Linux boot environment dataset. It automatically gets prefixed with `-R /mnt` since that's how we imported the pool.
1. We `zfs mount -a` which automounts `zpool/data/home` into `/home`, which again gets auto-prepended by `/mnt`.
1. We lastly mount our EFI partition into `/mnt/efi`.
1. We instruct ZFS to save its pool configuration via `zpool set cachefile=/etc/zfs/zpool.cache zpool`.
The complete ZFS structure now exists and is mounted at `/mnt` ready for any `pacstrap`, [debootstrap](https://wiki.debian.org/Debootstrap), `dnf --installroot` or other bootstrapping action.
## Adding another boot environment-independent dataset
Assume that in addition to your `/home` data which lives on `zpool/data/home` you want another dataset that is exempt from Arch Linux snapshots.
Consider an example `/opt/git` directory where a bunch of Git repos are checked out on which you work. You don't want them to be snapshotted - and rolled back - when something goes sideways: they are decoupled from everything else that goes on on your machine so you can easily and safely have a static `/opt/git` directory available in all boot environments.
Move your current `/opt/git` data out of the way for a moment:
```
mv '/opt/git'{,'.bak'}
```
Create datasets
```
zfs create -o canmount=off zpool/data/opt
zfs create zpool/data/opt/git
```
Remember that the `zpool/data` dataset already exists and that it has both `mountpoint=/` and `canmount=off` set. It is not and cannot be mounted itself, it instead conveniently anchors datasets at `/`. Since the `canmount` dataset property cannot be inherited and defaults to `canmount=on` we have to manually specify `-o canmount=off`. Our new `zpool/data/opt` should not automatically mount into `/opt`.
We then create the child dataset `zpool/data/opt/git`, it defaults to `canmount=on` thus immediately shows up at `/opt/git`.
Move data back into place and clean up temp directory
```
rsync -av --remove-source-files '/opt/git'{'.bak',}'/'
find '/opt/git.bak' -type d -empty -delete
```
An example `zpool/data` dataset may now look like so:
```
# zfs list -r -oname,mountpoint,canmount,mounted zpool/data
NAME MOUNTPOINT CANMOUNT MOUNTED
zpool/data / off no
zpool/data/home /home on yes
zpool/data/opt /opt off no
zpool/data/opt/git /opt/git on yes
```
## Nested environment-independent datasets
### Caution
If you want a dedicated dataset for a directory that lives deeper in your file system tree than just `/opt/git`, for example like `/var/lib/docker` make sure to not recursively create this structure in a single `zfs create` command.
In [Adding another boot environment-independent dataset](#adding-another-boot-environment-independent-dataset) above you can safely do:
```
zfs create -o canmount=off zpool/data/opt
```
Here `zpool/data` already exists, you're only creating one child dataset `opt` and you're setting `-o canmount=off` so that it never mounts into your `/opt` directory.
Now consider the same setup for `/var/lib/docker`. If you follow the exact same approach:
```
zfs create -o canmount=off zpool/data/var/lib
```
Docker will correctly report:
```
cannot create 'zpool/data/var/lib': parent does not exist
```
You might want to just create the parent then with `-p` argument:
```
zfs create -p -o canmount=off zpool/data/var/lib
~~
```
Note, however, that `-o canmount=off` only applies to `lib` dataset and that `zpool/data/var` has just been auto-mounted into `/var`:
```
# zfs list -r -oname,mountpoint,canmount,mounted zpool/data
NAME MOUNTPOINT CANMOUNT MOUNTED
zpool/data / off no
zpool/data/home /home on yes
zpool/data/opt /opt off no
zpool/data/opt/git /opt/git on yes
zpool/data/var /var on yes <---
zpool/data/var/lib /var/lib off no
```
### Advice
Instead create nested parents in multiple steps where you set each one to `-o canmount=off`:
```
zfs create -o canmount=off zpool/data/var
zfs create -o canmount=off zpool/data/var/lib
```
Lastly create the dataset you want mounted:
```
zfs create zpool/data/var/lib/docker
```
## Mounting zpool for maintenance
In case you want to mount your zpool on an external operating system such as an Arch Linux live CD ISO image do it like so:
```
zpool import zpool -d /dev/disk/by-partuuid -R /mnt -f -N
zfs load-key -L prompt zpool
zfs mount zpool/root/archlinux
zfs mount -a
mount /dev/sda1 /mnt/efi
arch-chroot /mnt /bin/bash
```
When done exit `chroot` and cleanly remove your work:
```
umount /mnt/efi
zfs umount -a
zpool export zpool
```
Explanation:
- We always want to mount pools `by-partuuid` for consistency so we specifically only look for pools at `/dev/disk/by-partuuid`.
- We mount our zpool with `-R /mnt` (aka `-o cachefile=none -o altroot=/mnt`). The pool is never cached, i.e. it's considered temporary. All pool and dataset mount paths have `/mnt` prepended. From `man zpoolprops`:
> This can be used when examining an unknown pool where the mount points cannot be trusted, or in an alternate boot environment, where the typical paths are not valid. `altroot` is not a persistent property. It is valid only while the system is up.
- With `-f` and `-N` we force-mount our pool (`-f`) even if it previously wasn't cleanly exported; and we do not auto-mount any of its datasets (`-N`), not even the ones that have `canmount=on` set.
```
# zfs list -oname,mountpoint,canmount,mounted
NAME MOUNTPOINT CANMOUNT MOUNTED
zpool none off no
zpool/data /mnt off no
zpool/data/home /mnt/home on no <-- Not immediately mounted
zpool/root none off no
zpool/root/archlinux /mnt noauto no <-- Not immediately mounted
```
- We load the decryption key by temporarily overriding the `keylocation` property to `-L prompt`. The default value is `file:///etc/zfs/zpool.key` which in all likelihood doesn't exist in this environment.
- We mount our desired boot environment with `zfs mount zpool/root/archlinux`
```
# zfs list -oname,mountpoint,canmount,mounted
NAME MOUNTPOINT CANMOUNT MOUNTED
zpool none off no
zpool/data /mnt off no
zpool/data/home /mnt/home on no
zpool/root none off no
zpool/root/archlinux /mnt noauto yes <-- Only boot env now mounted
```
- We mount all child datasets with `zfs mount -a` making `/mnt/home` available as well as any others you may have created yourself.
```
# zfs list -oname,mountpoint,canmount,mounted
NAME MOUNTPOINT CANMOUNT MOUNTED
zpool none off no
zpool/data /mnt off no
zpool/data/home /mnt/home on yes <-- Now mounted
zpool/root none off no
zpool/root/archlinux /mnt noauto yes <-- Now mounted
```
- We lastly mount our EFI system partition (ESP), in this example it's living at `/dev/sda1` so adjust this path accordingly.
```
# df -hTP
Filesystem Type Size Used Avail Use% Mounted on
... ... ... ... ... ... ...
zpool/root/archlinux zfs 8.6G 2.5G 6.2G 29% /mnt
zpool/data/home zfs 6.3G 161M 6.2G 3% /mnt/home
/dev/sda1 vfat 511M 31M 481M 6% /mnt/efi
```
- We're ready to `arch-chroot` into our boot environment.
# Development
## Conventional commits
@ -257,10 +495,10 @@ Commit _types_ besides `fix` and `feat` are:
The following _scopes_ are known for this project. A Conventional Commits commit message may optionally use one of the following scopes or none:
- `iso`: Changing Arch Linux ISO CD
- `iso`: Changing Arch Linux live CD ISO image
- `zbm`: Adjusting ZFSBootMenu's behavior
- `zfs`: A change to how ZFS interacts with the system, either a pool or a dataset
- `os`: Getting an perating system set up to correctly work in a ZFS boot environment
- `os`: Getting an operating system set up to correctly work in a ZFS boot environment
- `meta`: Affects the project's repo layout, readme content, file names etc.
# Credits

527
setup.sh
View File

@ -1,7 +1,24 @@
#!/bin/bash
# Whatever comes in on file descriptor (FD) 3 gets redirected to where file
# descriptor 1 is pointing. File descriptor 1 points to stdout so when we
# output-redirect something into FD 3 it shows up on stdout. We can use this
# to produce arbitrary logging output inside a subshell like so:
#
# function my_func () {
# some_command "${1:?}"
# >&3 echo 'A log message'
# }
#
# var="$(my_func arg_1)"
#
# Here "${var}" will only capture the output of some_command "${1:?}". It
# will not capture 'echo' which will instead show up on our stdout/FD 1.
exec 3>&1
declare this_script_url
this_script_url="${SCRIPT_URL:?}"
postconf_hook="$(dirname "${this_script_url}")"'/zbm_set_new_uefi_boot_entries.sh'
declare zpool_name zfs_arch_dataset_name
zpool_name='zpool'
@ -13,10 +30,6 @@ trap '[ "$?" -ne 77 ] || exit 77' ERR
declare zpool_drive efi_drive
function set_ntp () {
timedatectl set-ntp true
}
function we_are_changerooted () {
if [ "$(stat -c '%d:%i' '/')" != "$(stat -c '%d:%i' '/proc/1/root/.')" ]; then
return 0
@ -25,20 +38,38 @@ function we_are_changerooted () {
fi
}
function no_kernel_update_in_iso () {
#1.1
sed -ri -e 's'$'\x1''#(IgnorePkg)[^\r\n\f]+'$'\x1''\1 = linux linux-headers'$'\x1''g' /etc/pacman.conf
}
function set_ntp () {
#1.2
timedatectl set-ntp true
}
function resize_cow_space () {
#1.3
mount -o remount,size='50%' /run/archiso/cowspace
}
function update_pacman_db () {
#1.4
printf -- '%s\n' 'Refreshing mirror list ...'
systemctl start reflector
# In an ISO and for the minimal number of packages we need we do not
# care about partial upgrades
pacman -Sy
pacman -Syyuu --noconfirm
}
function install_pkgs () {
#1.5
printf -- '%s\n' 'Installing packages ...'
pacman -S --needed --noconfirm "${@}"
}
function install_zfs () {
#1.6
declare reset_colors='\033[0m'
curl -s 'https://raw.githubusercontent.com/eoli3n/archiso-zfs/master/init' | bash
printf -- "${reset_colors}"
@ -72,7 +103,7 @@ function get_parts () {
parts="$(get_partitions | jq --raw-output '.[][] | select(.children | length > 0) | select(.path=="'"${zfs_install_drive:?}"'") | .children[] | select(.parttype=="c12a7328-f81f-11d2-ba4b-00a0c93ec93b") | .path')" || exit 1
;;
*)
>2 printf -- '%s\n' 'Unknown partition type '"'"'"${parttype}"'"'"', exiting ...'
>&3 printf -- '%s\n' 'Unknown partition type '"'"'"${parttype}"'"'"', exiting ...'
exit 77
;;
esac
@ -88,7 +119,7 @@ function we_have_exactly_one_part () {
if [[ "$(wc -c <<<"${parts_list}")" -gt '1' ]]; then
case "${parts_count}" in
0)
>2 printf -- '%s\n' 'No '"${parttype^^}"' partition found. Exiting ...'
>&3 printf -- '%s\n' 'No '"${parttype^^}"' partition found. Exiting ...'
exit 77
;;
1)
@ -98,7 +129,7 @@ function we_have_exactly_one_part () {
return 1
;;
esac
>2 printf -- '%s\n' 'Partition list does not look valid. Cowardly exiting ...'
>&3 printf -- '%s\n' 'Partition list does not look valid. Cowardly exiting ...'
exit 77
fi
}
@ -111,7 +142,7 @@ function get_drive_id () {
printf -- '%s' "${drive_id_single}"
return 0
fi
>2 printf -- '%s\n' 'Not exactly one '"'${1:?}'"' partition entry in /dev/disk/by-id, exiting ...'
>&3 printf -- '%s\n' 'No '"'${1:?}'"' partition entry in /dev/disk/by-partuuid, exiting ...'
exit 77
}
@ -127,10 +158,26 @@ function select_part () {
parts="$(get_parts "${part_type}")"
fi
if [[ ! "${parts}" ]]; then
case "${part_type}" in
efi)
part_type_human_readable='EFI system partition (ESP) with partition type code EF00'
;;
zfs)
part_type_human_readable='ZFS zpool partition with partition type code BF00'
;;
esac
>&3 printf -- '%s\n' \
'It looks as if there is no '"${part_type_human_readable}" \
'on any of the disks. Did you correctly partition a disk before starting?' \
'Check https://quico.space/quico-os-setup/arch-zbm#prep. Exiting ...'
exit 77
fi
if we_have_exactly_one_part "${part_type}" "${parts}"; then
part="${parts}"
else
2> printf -- '%s\n' 'More than one '"${part_type^^}"' partition to pick for installation. Cowardly exiting ...'
>&3 printf -- '%s\n' 'More than one '"${part_type^^}"' partition to pick for installation. Cowardly exiting ...'
exit 77
fi
printf -- '%s' "${part}"
@ -151,22 +198,33 @@ function set_zpool_password () {
}
function import_pool () {
zpool import -d '/dev/disk/by-id' -R '/mnt' "${zpool_name}" -N -f
zfs load-key "${zpool_name}"
zpool import -d '/dev/disk/by-partuuid' -R '/mnt' "${zpool_name}" -N -f
[[ ! "${ARCHZBM_ZFSPROPS_NO_ENCRYPTION}" ]] && zfs load-key "${zpool_name}"
}
function create_pool () {
# Create a temporary pool that is not cached
#
# Add zfsprops 'compression' unless environment variable
# ARCHZBM_ZFSPROPS_NO_COMPRESSION is set to any value.
#
# Add zfsprops 'encryption' along with 'keyformat' and a 'keylocation'
# unless environment variable ARCHZBM_ZFSPROPS_NO_ENCRYPTION is set to
# any value.
zpool create -f \
-o 'ashift=12' \
-o 'autotrim=on' \
-O 'acltype=posix' \
-O 'compression=on' \
$([[ ! "${ARCHZBM_ZFSPROPS_NO_COMPRESSION}" ]] && \
printf -- '%s ' \
'-O compression=on') \
-O 'relatime=on' \
-O 'xattr=sa' \
-O 'encryption=on' \
-O 'keyformat=passphrase' \
-O 'keylocation=file:///etc/zfs/'"${zpool_name}"'.key' \
$([[ ! "${ARCHZBM_ZFSPROPS_NO_ENCRYPTION}" ]] && \
printf -- '%s ' \
'-O encryption=on' \
'-O keyformat=passphrase' \
'-O keylocation=file:///etc/zfs/'"${zpool_name}"'.key') \
-O 'normalization=formD' \
-O 'mountpoint=none' \
-O 'canmount=off' \
@ -176,7 +234,7 @@ function create_pool () {
}
function create_root_dataset () {
zfs create -o mountpoint=none "${zpool_name}"'/root'
zfs create -o 'mountpoint=none' -o 'canmount=off' "${zpool_name}"'/root'
# zfs set org.zfsbootmenu:commandline="ro quiet" "${zpool_name}"'/root'
zfs set org.zfsbootmenu:commandline="ro" "${zpool_name}"'/root'
}
@ -198,11 +256,12 @@ function export_pool () {
}
function setup_zpool () {
#1.7
local drive_by_id
zpool_drive="$(select_part 'zfs')"
drive_by_id="$(get_drive_id "${zpool_drive}")"
set_zpool_password
[[ ! "${ARCHZBM_ZFSPROPS_NO_ENCRYPTION}" ]] && set_zpool_password
if no_zpool_exists; then
create_pool "${drive_by_id}"
create_root_dataset
@ -211,12 +270,13 @@ function setup_zpool () {
export_pool
import_pool
else
printf -- '%s\n' 'A zpool already exists, that is unexpected. Cowardly exiting 1 ...'
>&3 printf -- '%s\n' 'A zpool already exists, that is unexpected. Cowardly exiting 1 ...'
exit 1
fi
}
function mount_system () {
#1.8
zfs mount "${zpool_name}"'/root/'"${zfs_arch_dataset_name}"
zfs mount -a
@ -229,16 +289,29 @@ function mount_system () {
}
function copy_zpool_cache () {
#1.9
mkdir -p '/mnt/etc/zfs'
zpool set 'cachefile=/etc/zfs/'"${zpool_name}"'.cache' "${zpool_name}"
}
function pacman_dl_parallel () {
sed -ri -e 's'$'\x1''^.*?(ParallelDownloads)[^\r\n\f]*'$'\x1''\1 = 5'$'\x1''g' '/etc/pacman.conf'
function pacman_dont_check_space () {
# See pacman bug comment
# https://bugs.archlinux.org/task/45070#comment142712
#
# When we pacstrap onto ZFS pacman incorrectly calculates and
# overestimates required disk space. We instead assume an installation
# gets done with at least a 10 GiB drive which is plenty. Skip pacman's
# size check.
#
# We're setting this in Arch Linux ISO CD while we install proper Arch.
# No need to revert this later as it is ephemeral anyway.
sed -ri -e 's'$'\x1''^.*?(CheckSpace)([^\r\n\f]*)'$'\x1''#\1\2'$'\x1''g' '/etc/pacman.conf'
}
function install_archlinux () {
#1.10
pacman_dl_parallel
pacman_dont_check_space
pacstrap /mnt \
base \
base-devel \
@ -263,6 +336,7 @@ function install_archlinux () {
}
function gen_fstab () {
#1.11
genfstab -U /mnt | grep -v "${zpool_name}" | tr -s '\n' | sed -r -e 's/\/mnt//' -e '/./,$!d' > '/mnt/etc/fstab'
}
@ -275,6 +349,7 @@ EOF
}
function set_hostname () {
#1.12
declare new_hostname
install_pkgs 'pwgen'
new_hostname="$(pwgen --no-numerals --no-capitalize --ambiguous 8)"
@ -283,6 +358,7 @@ function set_hostname () {
}
function set_locale () {
#1.13
printf -- '%s\n' \
'KEYMAP=de-latin1' \
'FONT=Lat2-Terminus16' \
@ -293,17 +369,22 @@ function set_locale () {
}
function add_zfs_hook_to_initramfs () {
# Add zfs hook, remove fsck hook from initramfs. Also add plain text key
# file into initramfs since it's living inside an encrypted pool anyway.
#1.14
# Add zfs hook, remove fsck hook from initramfs.
sed -ri \
-e 's'$'\x1''^(FILES=)[^\r\n\f]*'$'\x1''\1(/etc/zfs/'"${zpool_name}"'.key)'$'\x1''g' \
-e 's'$'\x1''(HOOKS=)(.*?[\(| ])(filesystems)([\)| ][^\r\n\f]*)'$'\x1''\1\2zfs \3\4'$'\x1''g' \
-e 's'$'\x1''((\()(fsck)(\)))'$'\x1''\2\4'$'\x1''g' \
-e 's'$'\x1''(([[:space:]]+)(fsck)|(fsck)([[:space:]]+))'$'\x1'''$'\x1''g' \
'/mnt/etc/mkinitcpio.conf'
# Also unless encryption's unwanted add plain text key file into
# initramfs since it's living inside an encrypted pool anyway.
[[ ! "${ARCHZBM_ZFSPROPS_NO_ENCRYPTION}" ]] && sed -ri \
-e 's'$'\x1''^(FILES=)[^\r\n\f]*'$'\x1''\1(/etc/zfs/'"${zpool_name}"'.key)'$'\x1''g' \
'/mnt/etc/mkinitcpio.conf'
}
function set_initramfs_build_list () {
#1.15
# No need to build fallback initramfs, our new fallback is ZFS snapshots
sed -ri \
-e '/^#/d' \
@ -317,65 +398,12 @@ function set_initramfs_build_list () {
}
function add_zfs_files_to_new_os () {
for zfs_file in '/etc/hostid' '/etc/zfs/zpool.cache' '/etc/zfs/'"${zpool_name}"'.key'; do
#1.16
for zfs_file in '/etc/hostid' '/etc/zfs/zpool.cache' $([[ ! "${ARCHZBM_ZFSPROPS_NO_ENCRYPTION}" ]] && printf -- '%s' '/etc/zfs/'"${zpool_name}"'.key'); do
rsync -av --itemize-changes {'','/mnt'}"${zfs_file}"
done
}
function enter_chroot () {
arch-chroot /mnt /bin/bash -xe <<EOF
curl --silent '${this_script_url}' | bash
EOF
}
function get_pkg_info () {
declare from_where local_pkg_info local_pkg_version version_search
from_where="${1}"
version_search='/Version/{print $3}'
pkg_info="$(paru -$([[ "${from_where}" == 'local' ]] && printf -- '%s' 'Q' || printf -- '%s' 'S')i "${2}" 2>&1)"
if [[ "${from_where}" == 'local' ]] && grep -Piq -- '^error: package .*? was not found' <<<"${pkg_info}"; then
return 1
else
local_pkg_version="$(awk "${version_search}" <<<"${pkg_info}")"
fi
printf -- '%s' "${local_pkg_version}"
return 0
}
function paru_with_zfs_first () {
if [[ "${#}" -eq '0' ]]; then
declare -A local_pkg_info
/usr/bin/paru -Sy
if local_pkg_info['zfs-dkms']="$(get_pkg_info 'local' 'zfs-dkms')" && local_pkg_info['zfs-utils']="$(get_pkg_info 'local' 'zfs-utils')"; then
local_pkg_info['zfs-dkms']="$(get_pkg_info 'local' 'zfs-dkms')"
local_pkg_info['zfs-utils']="$(get_pkg_info 'local' 'zfs-utils')"
declare -A remote_pkg_info
remote_pkg_info['zfs-dkms']="$(get_pkg_info 'remote' 'zfs-dkms')"
remote_pkg_info['zfs-utils']="$(get_pkg_info 'remote' 'zfs-utils')"
/usr/bin/paru -S --needed archlinux-keyring
if [[ "${local_pkg_info['zfs-dkms']}" == "${remote_pkg_info['zfs-dkms']}" ]] && \
[[ "${local_pkg_info['zfs-utils']}" == "${remote_pkg_info['zfs-utils']}" ]]; then
/usr/bin/paru -Su
else
/usr/bin/paru -Sy 'zfs-dkms' 'zfs-utils' \
--assume-installed zfs-dkms="${local_pkg_info['zfs-dkms']}" \
--assume-installed zfs-dkms="${remote_pkg_info['zfs-dkms']}" \
--assume-installed zfs-utils="${local_pkg_info['zfs-utils']}" \
--assume-installed zfs-utils="${remote_pkg_info['zfs-utils']}"
/usr/bin/paru -Su
fi
else
/usr/bin/paru -S --needed archlinux-keyring
/usr/bin/paru -Su
fi
else
/usr/bin/paru "${@}"
fi
}
function create_unpriv_user () {
account_name="${1:?}"
full_name="${2:-${account_name}}"
@ -387,37 +415,15 @@ function create_unpriv_user () {
chown -R "${account_name}"':' '/home/'"${account_name}"; chmod -R 'u=rwX,go=' "$(dirname "${authorized_keys_abs_path}")"
}
function unleash_makepkg () {
local path_prefix
path_prefix='/mnt'
if we_are_changerooted; then
path_prefix=''
fi
sed -ri \
-e 's'$'\x1''^(#?(MAKEFLAGS=))[^\r\n\f]*'$'\x1''\2"-j$(nproc --ignore 1)"'$'\x1''g' \
"${path_prefix}"'/etc/makepkg.conf'
}
function get_aur_helper () {
create_unpriv_user 'build'
usermod --append --groups 'wheel' 'build'
printf -- '%s\n' '%wheel ALL=(ALL:ALL) NOPASSWD: ALL' > '/etc/sudoers.d/10-wheel-group-no-passwd-prompt'
pushd /tmp
git clone 'https://aur.archlinux.org/paru.git'
chown -R 'build:' 'paru'
pushd 'paru'
sudo --user 'build' makepkg -si --noconfirm
popd
rm -rf 'paru'
popd
alias paru='paru_with_zfs_first'
}
function paru_install () {
sudo --user build paru -S --noconfirm "${@}"
function enter_chroot () {
#2.1
arch-chroot /mnt /bin/bash -xe <<EOF
curl --silent '${this_script_url}' | bash
EOF
}
function keep_initiramfs_root_only_rw () {
#2.3
declare systemd_local_admin_override_path unit_name
systemd_local_admin_override_path='/etc/systemd/system'
unit_name='chmod-initramfs'
@ -448,7 +454,27 @@ EOF
systemctl enable "${path_unit}"
}
function pacman_dl_parallel () {
#2.4
# We're setting this in Arch Linux ISO CD while we install proper Arch.
# No need to revert this later as it is ephemeral anyway.
sed -ri -e 's'$'\x1''^.*?(ParallelDownloads)[^\r\n\f]*'$'\x1''\1 = 20'$'\x1''g' '/etc/pacman.conf'
}
function unleash_makepkg () {
#2.5
local path_prefix
path_prefix='/mnt'
if we_are_changerooted; then
path_prefix=''
fi
sed -ri \
-e 's'$'\x1''^(#?(MAKEFLAGS=))[^\r\n\f]*'$'\x1''\2"-j$(nproc --ignore 1)"'$'\x1''g' \
"${path_prefix}"'/etc/makepkg.conf'
}
function add_motd_getting_started_msg () {
#2.6
cat > '/etc/motd' <<"EOF"
####################
@ -457,13 +483,121 @@ GUI basics:
paru -S xorg plasma-meta kde-applications-meta sddm
localectl set-x11-keymap de
useradd --create-home --shell /bin/bash --user-group --groups wheel <user>
passwd <user>
systemctl enable --now sddm.service
####################
EOF
}
function get_aur_helper () {
#2.7
create_unpriv_user 'build'
usermod --append --groups 'wheel' 'build'
printf -- '%s\n' '%wheel ALL=(ALL:ALL) NOPASSWD: ALL' > '/etc/sudoers.d/10-wheel-group-no-passwd-prompt'
pushd /tmp
git clone 'https://aur.archlinux.org/paru.git'
chown -R 'build:' 'paru'
pushd 'paru'
sudo --user 'build' makepkg -si --noconfirm
popd
rm -rf 'paru'
popd
}
function paru_install () {
declare -a paru_install_packages
[[ "${1}" ]] && while :; do
case "${1}" in
-[[:alnum:]]*)
>&3 printf -- '%s\n' \
'Short-form argument '"'${1}'"' not supported for function '"'${FUNCNAME[0]}()'"'. Only known accepted argument' \
'is '"'"'--replace-conflicting'"'"' without a value given. Exiting ...'
exit 77
;;
--replace-conflicting)
pacman_force_yes='true'
shift
continue
;;
--*)
>&3 printf -- '%s\n' \
'Long-form argument '"'${1}'"' not supported for function '"'${FUNCNAME[0]}()'"'. Only known accepted argument' \
'is '"'"'--replace-conflicting'"'"' without a value given. Exiting ...'
exit 77
;;
'')
# All arguments processed
break
;;
*)
paru_install_packages+=("${1}")
shift
;;
esac
done || {
>&3 printf -- '%s\n' \
'No argument '"'${1}'"' given for function '"'${FUNCNAME[0]}'"'. Exiting ...'
exit 77
}
if [[ "${pacman_force_yes}" ]]; then
yes 'y' | sudo --user 'build' paru -S "${paru_install_packages[@]}"
unset -v pacman_force_yes
else
sudo --user 'build' paru -S --noconfirm "${paru_install_packages[@]}"
fi
}
function configure_zfsbootmenu () {
#2.9
paru_install 'zfsbootmenu'
mkdir -p '/etc/zfsbootmenu/posthooks.d'
cat > '/etc/zfsbootmenu/config.yaml' <<EOF
Global:
ManageImages: true
BootMountPoint: /efi
InitCPIO: true
PostHooksDir: /etc/zfsbootmenu/posthooks.d
Components:
Enabled: false
EFI:
ImageDir: /efi/EFI/ZBM
Versions: false
Enabled: true
Stub: /etc/zfsbootmenu/stub-loader.d/linuxx64.efi.stub
Kernel:
CommandLine: ro loglevel=0 zbm.import_policy=hostid
Prefix: vmlinuz
EOF
# Up here maybe 'ro quiet' instead of 'ro'
zfs set org.zfsbootmenu:commandline='rw nowatchdog rd.vconsole.keymap=de-latin1' "${zpool_name}"'/root/'"${zfs_arch_dataset_name}"
}
function get_known_good_stub_loader () {
local known_good_stub_loader local_stub_loader_abs
known_good_stub_loader='https://github.com/zbm-dev/zfsbootmenu/raw/master/testing/stubs/linuxx64.efi.stub'
local local_stub_loader_abs='/etc/zfsbootmenu/stub-loader.d/linuxx64.efi.stub'
mkdir -p "$(dirname "${local_stub_loader_abs}")"
curl --silent --location "${known_good_stub_loader}" --output "${local_stub_loader_abs}"
}
function get_disks_with_one_efipart () {
local disks_with_one_efipart
# Find disks that have exactly one EFI partition and where that EFI
# partition is partition number 1. We expect exactly one disk to meet
# these criteria. Anything else and we bail.
disks_with_one_efipart="$(lsblk --output PATH,PARTTYPE --json --tree | jq --raw-output '.[][] | select(.children | length > 0) | select( any (.children[]; (.path | test("^[^[:digit:]]+1")) and (.parttype=="c12a7328-f81f-11d2-ba4b-00a0c93ec93b")) and ([select(.children[].parttype=="c12a7328-f81f-11d2-ba4b-00a0c93ec93b")] | length == 1) ) | .path')"
if [[ "$(wc -l <<<"${disks_with_one_efipart}")" -eq '1' ]] && [[ "$(wc -c <<<"${disks_with_one_efipart}")" -gt '1' ]]; then
printf -- '%s' "${disks_with_one_efipart}"
return 0
fi
return 1
}
function install_os_in_chroot () {
#2.2
### Reinit keyring
# As keyring is initialized at boot, and copied to the install dir with pacstrap, and ntp is running
# Time changed after keyring initialization, it leads to malfunction
@ -473,33 +607,32 @@ function install_os_in_chroot () {
pacman-key --populate archlinux
pacman -S archlinux-keyring --noconfirm
keep_initiramfs_root_only_rw
pacman_dl_parallel
unleash_makepkg
add_motd_getting_started_msg
get_aur_helper
paru_install 'zfs-dkms' 'zfs-utils'
hwclock --systohc
locale-gen
source /etc/locale.conf
keep_initiramfs_root_only_rw #2.3
pacman_dl_parallel #2.4
unleash_makepkg #2.5
add_motd_getting_started_msg #2.6
get_aur_helper #2.7
paru_install --replace-conflicting 'paru-bin'
paru_install 'zfs-dkms' 'zfs-utils' 'jq'
hwclock --systohc
mkinitcpio -P
# Install ZFSBootMenu and deps
git clone --depth=1 https://github.com/zbm-dev/zfsbootmenu/ '/tmp/zfsbootmenu'
paru_install 'cpanminus' 'kexec-tools' 'fzf' 'util-linux'
pushd '/tmp/zfsbootmenu'
make
make install
cpanm --notest --installdeps .
popd
rm -rf '/tmp/zfsbootmenu'
# Install ZFSBootMenu image
configure_zfsbootmenu #2.9
get_known_good_stub_loader #2.10
generate-zbm
}
function set_root_pw () {
printf -- '%s\n' 'root:password' | chpasswd --root '/mnt'
#3.2
printf -- '%s\n' 'root:password' | chpasswd --crypt-method 'SHA512' --root '/mnt'
}
function configure_networking () {
#3.3
cat > '/mnt/etc/systemd/network/50-wired.network' <<"EOF"
[Match]
Name=en*
@ -517,6 +650,7 @@ EOF
}
function configure_dns () {
#3.4
rm '/mnt/etc/resolv.conf'
ln -s '/run/systemd/resolve/stub-resolv.conf' '/mnt/etc/resolv.conf'
@ -527,119 +661,116 @@ function configure_dns () {
}
function configure_reflector () {
systemctl enable 'reflector' --root='/mnt'
#3.5
systemctl enable 'reflector.service' 'reflector.timer' --root='/mnt'
}
function configure_zfs () {
#3.6
systemctl enable 'zfs-import-cache' 'zfs-mount' 'zfs-import.target' 'zfs.target' --root='/mnt'
}
function configure_zfs_mount_gen () {
#3.7
mkdir -p '/mnt/etc/zfs/zfs-list.cache'
touch '/mnt/etc/zfs/zfs-list.cache/'"${zpool_name}"
zfs list -H -o name,mountpoint,canmount,atime,relatime,devices,exec,readonly,setuid,nbmand | sed 's/\/mnt//' > '/mnt/etc/zfs/zfs-list.cache/'"${zpool_name}"
systemctl enable 'zfs-zed.service' --root='/mnt'
}
function configure_zfsbootmenu () {
curl -s 'https://raw.githubusercontent.com/zbm-dev/zfsbootmenu/master/etc/zfsbootmenu/mkinitcpio.conf' | sed -r -e '/^#/d' -e '/^$/d' > '/mnt/etc/zfsbootmenu/mkinitcpio.conf'
cat > '/mnt/etc/zfsbootmenu/config.yaml' <<EOF
Global:
ManageImages: true
BootMountPoint: /efi
InitCPIO: true
function set_new_uefi_boot_entries () {
#3.8
declare -a uefi_images
mapfile -t uefi_images < \
<(find '/mnt/efi/EFI/ZBM' -type f -iname '*.efi' -print0 | \
xargs -0 --no-run-if-empty --max-args '1' stat -c '%Y %n' | \
sort -V | \
awk '{print $2}')
Components:
Enabled: false
EFI:
ImageDir: /efi/EFI/ZBM
Versions: false
Enabled: true
Kernel:
CommandLine: ro loglevel=0 zbm.import_policy=hostid
Prefix: vmlinuz
EOF
# Up here maybe 'ro quiet' instead of 'ro'
zfs set org.zfsbootmenu:commandline='rw nowatchdog rd.vconsole.keymap=de-latin1' "${zpool_name}"'/root/'"${zfs_arch_dataset_name}"
}
function gen_zfsbootmenu () {
arch-chroot /mnt /bin/bash -xe <<"EOF"
source /etc/locale.conf
mkdir -p '/efi/EFI/ZBM'
find /efi/EFI/ZBM -type f -delete
generate-zbm
EOF
}
function get_disks_with_one_efipart () {
local disks_with_one_efipart
# Find disks that have exactly one EFI partition and where that EFI
# partition is partition number 1. We expect exactly one disk to meet
# these criteria. Anything else and we bail.
disks_with_one_efipart="$(lsblk --output PATH,PARTTYPE --json --tree | jq --raw-output '.[][] | select(.children | length > 0) | select( any (.children[]; (.path | test("^[^[:digit:]]+1")) and (.parttype=="c12a7328-f81f-11d2-ba4b-00a0c93ec93b")) and ([select(.children[].parttype=="c12a7328-f81f-11d2-ba4b-00a0c93ec93b")] | length == 1) ) | .path')"
if [[ "$(wc -l <<<"${disks_with_one_efipart}")" -eq '1' ]] && [[ "$(wc -c <<<"${disks_with_one_efipart}")" -gt '1' ]]; then
printf -- '%s' "${disks_with_one_efipart}"
return 0
if efibootmgr | grep -Piq -- 'ZFSBootMenu'; then
local -a old_uefi_entries
mapfile -t old_uefi_entries < \
<(efibootmgr | \
grep -Pio -- '(?<=^Boot)[^\*[:space:]]+(?=\*? ZFSBootMenu)')
for old_uefi_entry in "${old_uefi_entries[@]}"; do
efibootmgr --bootnum "${old_uefi_entry}" --delete-bootnum &>/dev/null && {
>&3 printf -- '%s\n' \
'EFI boot entry '"${old_uefi_entry}"' deleted.'
}
done
fi
return 1
}
function add_zbm_to_efi () {
if ! efibootmgr | grep -Pi -- 'ZFSBootMenu'; then
if ! efibootmgr | grep -Piq -- 'ZFSBootMenu'; then
local efi_disks_list
efi_disks_list="$(get_disks_with_one_efipart)"
if grep -Piq -- '^'"${efi_drive}"'$' <<<"${efi_disks_list}"; then
for uefi_image in "${uefi_images[@]}"; do
uefi_image_version="$(basename "${uefi_image%%.EFI}")"
uefi_image_inverted="${uefi_image#/mnt/efi}"
uefi_image_inverted="${uefi_image_inverted//\//\\}"
efibootmgr --disk "${efi_drive}" \
--part 1 \
--create \
--label "ZFSBootMenu" \
--loader "\EFI\ZBM\vmlinuz.efi" \
--verbose
--label 'ZFSBootMenu '"${uefi_image_version}" \
--loader "${uefi_image_inverted}" &>/dev/null && {
>&3 printf -- '%s\n' \
'EFI boot entry ZFSBootMenu '"${uefi_image_version}"' added.'
}
done
fi
fi
}
function insert_zbm_postconf_hook () {
#3.9
declare postconf_target_abs='/mnt/etc/zfsbootmenu/posthooks.d/'"$(basename "${postconf_hook}")"
curl --silent --location "${postconf_hook}" --output "${postconf_target_abs}"
chmod +x "${postconf_target_abs}"
}
function umount_all () {
#3.10
umount '/mnt/efi'
zfs umount -a
zpool export "${zpool_name}"
}
function finalize_os_setup () {
set_root_pw
configure_networking
configure_dns
configure_reflector
configure_zfs
configure_zfs_mount_gen
configure_zfsbootmenu
gen_zfsbootmenu
add_zbm_to_efi
umount_all
#3.1
set_root_pw #3.2
configure_networking #3.3
configure_dns #3.4
configure_reflector #3.5
configure_zfs #3.6
configure_zfs_mount_gen #3.7
set_new_uefi_boot_entries #3.8
insert_zbm_postconf_hook #3.9
umount_all #3.10
}
function main () {
if we_are_changerooted; then
install_os_in_chroot
install_os_in_chroot #2.2
else
set_ntp
update_pacman_db
install_pkgs 'jq'
install_zfs
setup_zpool
mount_system
copy_zpool_cache
install_archlinux
gen_fstab
set_hostname
set_locale
add_zfs_hook_to_initramfs
set_initramfs_build_list
add_zfs_files_to_new_os
enter_chroot
no_kernel_update_in_iso #1.1
set_ntp #1.2
resize_cow_space #1.3
update_pacman_db #1.4
install_pkgs 'base-devel' 'git' 'jq' #1.5
install_zfs #1.6
setup_zpool #1.7
mount_system #1.8
copy_zpool_cache #1.9
install_archlinux #1.10
gen_fstab #1.11
set_hostname #1.12
set_locale #1.13
add_zfs_hook_to_initramfs #1.14
set_initramfs_build_list #1.15
add_zfs_files_to_new_os #1.16
enter_chroot #2.1
# We're done in chroot
finalize_os_setup
finalize_os_setup #3.1
fi
}

View File

@ -0,0 +1,177 @@
#!/bin/bash
# Whatever comes in on file descriptor (FD) 3 gets redirected to where file
# descriptor 1 is pointing. File descriptor 1 points to stdout so when we
# output-redirect something into FD 3 it shows up on stdout. We can use this
# to produce arbitrary logging output inside a subshell like so:
#
# function my_func () {
# some_command "${1:?}"
# >&3 echo 'A log message'
# }
#
# var="$(my_func arg_1)"
#
# Here "${var}" will only capture the output of some_command "${1:?}". It
# will not capture 'echo' which will instead show up on our stdout/FD 1.
exec 3>&1
# https://unix.stackexchange.com/a/48550
set -E
trap '[ "$?" -ne 77 ] || exit 77' ERR
function get_partitions () {
declare partitions_json
partitions_json="$(lsblk --output PATH,PARTTYPE --json --tree)" || return 1
printf -- '%s' "${partitions_json}"
return 0
}
function get_parts () {
local zfs_install_drive
declare parttype parts
parttype="${1:?}"
zfs_install_drive="${2:-}"
case "${parttype}" in
zfs)
parts="$(get_partitions | jq --raw-output '.[][] | select(.children | length > 0) | .children[] | select(.parttype=="6a85cf4d-1dd2-11b2-99a6-080020736631") | .path')" || exit 1
;;
efi)
parts="$(get_partitions | jq --raw-output '.[][] | select(.children | length > 0) | select(.path=="'"${zfs_install_drive:?}"'") | .children[] | select(.parttype=="c12a7328-f81f-11d2-ba4b-00a0c93ec93b") | .path')" || exit 1
;;
*)
>&3 printf -- '%s\n' 'Unknown partition type '"'"'"${parttype}"'"'"', exiting ...'
exit 77
;;
esac
printf -- '%s' "${parts}"
return 0
}
function we_have_exactly_one_part () {
local parttype parts_list parts_count
parttype="${1:?}"
parts_list="${2:?}"
parts_count="$(wc -l <<<"${parts_list}")"
if [[ "$(wc -c <<<"${parts_list}")" -gt '1' ]]; then
case "${parts_count}" in
0)
>&3 printf -- '%s\n' 'No '"${parttype^^}"' partition found. Exiting ...'
exit 77
;;
1)
return 0
;;
*)
return 1
;;
esac
>&3 printf -- '%s\n' 'Partition list does not look valid. Cowardly exiting ...'
exit 77
fi
}
function select_part () {
local parts enriched_parts enriched_parts_count part_number part_type zfs_install_drive
declare part
part_type="${1:?}" # 'efi' or 'zfs'
zfs_install_drive="${2:-}"
if [[ "${zfs_install_drive}" ]]; then
# This is intended to find correct EFI partition
parts="$(get_parts "${part_type}" "${zfs_install_drive}")"
else
parts="$(get_parts "${part_type}")"
fi
if [[ ! "${parts}" ]]; then
case "${part_type}" in
efi)
part_type_human_readable='EFI system partition (ESP) with partition type code EF00'
;;
zfs)
part_type_human_readable='ZFS zpool partition with partition type code BF00'
;;
esac
>&3 printf -- '%s\n' \
'It looks as if there is no '"${part_type_human_readable}" \
'on any of the disks. Is this a chroot? Exiting ...'
exit 77
fi
if we_have_exactly_one_part "${part_type}" "${parts}"; then
part="${parts}"
else
>&3 printf -- '%s\n' 'More than one '"${part_type^^}"' partition to pick for installation. Cowardly exiting ...'
exit 77
fi
printf -- '%s' "${part}"
return 0
}
function get_part_parent () {
local child_partition parent_partition
child_partition="${1:?}"
parent_partition="$(get_partitions | jq --raw-output '.[][] | select(.children | length > 0) | select(.children[].path=="'"${child_partition:?}"'") | .path')"
printf -- '%s' "${parent_partition}"
return 0
}
function get_disks_with_one_efipart () {
local disks_with_one_efipart
# Find disks that have exactly one EFI partition and where that EFI
# partition is partition number 1. We expect exactly one disk to meet
# these criteria. Anything else and we bail.
disks_with_one_efipart="$(lsblk --output PATH,PARTTYPE --json --tree | jq --raw-output '.[][] | select(.children | length > 0) | select( any (.children[]; (.path | test("^[^[:digit:]]+1")) and (.parttype=="c12a7328-f81f-11d2-ba4b-00a0c93ec93b")) and ([select(.children[].parttype=="c12a7328-f81f-11d2-ba4b-00a0c93ec93b")] | length == 1) ) | .path')"
if [[ "$(wc -l <<<"${disks_with_one_efipart}")" -eq '1' ]] && [[ "$(wc -c <<<"${disks_with_one_efipart}")" -gt '1' ]]; then
printf -- '%s' "${disks_with_one_efipart}"
return 0
fi
return 1
}
function set_new_uefi_boot_entries () {
declare -a uefi_images
mapfile -t uefi_images < \
<(find '/efi/EFI/ZBM' -type f -iname '*.efi' -print0 | \
xargs -0 --no-run-if-empty --max-args '1' stat -c '%Y %n' | \
sort -V | \
awk '{print $2}')
zpool_drive="$(select_part 'zfs')"
zfs_parent="$(get_part_parent "${zpool_drive:?}")"
efi_drive="${zfs_parent}"
if efibootmgr | grep -Piq -- 'ZFSBootMenu'; then
local -a old_uefi_entries
mapfile -t old_uefi_entries < \
<(efibootmgr | \
grep -Pio -- '(?<=^Boot)[^\*[:space:]]+(?=\*? ZFSBootMenu)')
for old_uefi_entry in "${old_uefi_entries[@]}"; do
efibootmgr --bootnum "${old_uefi_entry}" --delete-bootnum &>/dev/null && {
>&3 printf -- '%s\n' \
'EFI boot entry '"${old_uefi_entry}"' deleted.'
}
done
fi
if ! efibootmgr | grep -Piq -- 'ZFSBootMenu'; then
local efi_disks_list
efi_disks_list="$(get_disks_with_one_efipart)"
if grep -Piq -- '^'"${efi_drive}"'$' <<<"${efi_disks_list}"; then
for uefi_image in "${uefi_images[@]}"; do
uefi_image_version="$(basename "${uefi_image%%.EFI}")"
uefi_image_inverted="${uefi_image#/efi}"
uefi_image_inverted="${uefi_image_inverted//\//\\}"
efibootmgr --disk "${efi_drive}" \
--part 1 \
--create \
--label 'ZFSBootMenu '"${uefi_image_version}" \
--loader "${uefi_image_inverted}" &>/dev/null && {
>&3 printf -- '%s\n' \
'EFI boot entry ZFSBootMenu '"${uefi_image_version}"' added.'
}
done
fi
fi
}
set_new_uefi_boot_entries