Compare commits

..

12 Commits

2 changed files with 211 additions and 39 deletions

View File

@@ -99,6 +99,14 @@ The script will create a single ZFS zpool `zpool` on the zpool partition with da
## Options
The following options can be given either by exporting them as shell variables prior to script execution or in a file named `archzbm_settings.env` that lives in your current working directory where you're about to execute the script. File format is identical to shell variable assignments of the form `VAR=value` or `VAR='value'`.
If `./archzbm_settings.env` exists the script will `source` its content and `export` all variables for use in future steps.
In cases where a variable is both exported prior to script execution and specified in `archzbm_settings.env` the latter will override the former.
Known options are as follows.
### 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>`.
@@ -119,24 +127,95 @@ export ARCHZBM_ZFSPROPS_NO_ENCRYPTION=yup
### Passwords
By default both the zpool password and the account password for `root` are literally `password`. While you can certainly change these after initial system setup you can also optionally set these passwords in a settings file named `archzbm_settings.env` that lives in your current working directory where you're about to execute the script. File format is identical to shell variable assignments of the form `VAR=value` or `VAR='value'`.
If `./archzbm_settings.env` exists the script will `source` its content and `export` all variables for use in future steps. Only known variables are:
By default both the zpool password and the account password for `root` are literally `password`. While you can certainly change these after initial system setup (see [Password change](#password-change)) you can also optionally set passwords as follows:
```
ARCHZBM_ZPOOL_PASSWORD='a fancy password'
ARCHZBM_ROOT_PASSWORD='t0psecr3t!'
```
> While the `root` password may be weak and `chpasswd` won't care do make sure to set a zpool password that meets ZFS' complexity rules. Per `man 7 zfsprops` section `keyformat` the only requirement is a length "between 8 and 512 bytes" (as in minimum 8 characters). If you pick a password that's too weak ZFS will reject zpool creation and very ungracefully derail the rest of this script. The script doesn't check what you're setting.
> While the `root` password is allowed to be weak and `chpasswd` won't care do make sure to set a zpool password that meets ZFS' complexity rules. Per `man 7 zfsprops` section `keyformat` the only requirement is a length "between 8 and 512 bytes" (as in minimum 8 characters). If you pick a password that's too weak ZFS will reject zpool creation and very ungracefully derail the rest of this script. The script doesn't check what you're setting.
The script does create a second user named `build` but doesn't set a password on account creation. As such no password variable can be set for it in `./archzbm_settings.env`. It's intended as a helper for system setup tasks such as `sudo -u build paru -S <package>` where an account password is irrelevant since `root` can always `sudo` whatever it wants. You will not be able to log in to the `build` account yourself although you certainly could set a password for it. Instead we suggest you create a proper user account for yourself. Your newly installed Arch Linux comes with an `/etc/motd` greeting that summarizes this as:
The script does create a second user named `build` but doesn't set a password on account creation. It's intended as a helper for system setup tasks such as `sudo -u build paru -S <package>` where an account password is irrelevant since `root` can always `sudo` whatever it wants. You will not be able to log in to the `build` account yourself although you certainly could set a password for it. Instead we suggest you create a proper user account for yourself. Your newly installed Arch Linux comes with an `/etc/motd` greeting that summarizes this as:
```
useradd --create-home --shell /bin/bash --user-group --groups wheel <user>
passwd <user>
```
### Networking
By default the script configures plain ZFSBootMenu without networking nor an SSH server. If you're interested in SSH-ing into your ZFSBootMenu boot loader you're going to want to specify some of the following variables.
#### IP address
```
ARCHZBM_NET_CLIENT_IP=''
ARCHZBM_NET_SERVER_IP=''
ARCHZBM_NET_GATEWAY_IP=''
ARCHZBM_NET_NETMASK=''
ARCHZBM_NET_HOSTNAME=''
ARCHZBM_NET_DEVICE=''
ARCHZBM_NET_AUTOCONF=''
```
By default none of the variables are set to any value and no networking will be available in ZFSBootMenu. If you want networking as in an IP address bound to a network interace set at least one of these variables or one of the [SSH](#ssh) variables listed further down. Setting one or more `ARCHZBM_NET_*` variables to an empty string is valid. If at least one variable is given either from this paragraph or from [SSH](#ssh) we're assuming that you want networking. Unspecified values and values set to the empty string `''` use defaults.
For networking we rely on the [mkinitcpio-nfs-utils](https://archlinux.org/packages/core/x86_64/mkinitcpio-nfs-utils/) package with its `net` hook. Please refer to its [initcpio-install-net](https://gitlab.archlinux.org/archlinux/packaging/packages/mkinitcpio-nfs-utils/-/blob/main/initcpio-install-net) script file for usage hints on above variables. The hook implements a subset of the [ip Kernel Command Line argument](https://docs.kernel.org/admin-guide/nfs/nfsroot.html).
Mapping between `net` hook field names and our shell variables is straightforward. Fields 8, 9 and 10 (DNS and NTP server addresses) from the official `ip` docs are unsupported in `net` hook. As such our hook has a total of 7 fields available for you to configure.
```
+-------------+------------------------+
| net hook | This script |
+-------------+------------------------+
| <client-ip> | ARCHZBM_NET_CLIENT_IP |
| <server-ip> | ARCHZBM_NET_SERVER_IP |
| <gw-ip> | ARCHZBM_NET_GATEWAY_IP |
| <netmask> | ARCHZBM_NET_NETMASK |
| <hostname> | ARCHZBM_NET_HOSTNAME |
| <device> | ARCHZBM_NET_DEVICE |
| <autoconf> | ARCHZBM_NET_AUTOCONF |
+-------------+------------------------+
```
A valid example with a few fields populated may look like so:
```
ARCHZBM_NET_CLIENT_IP='10.10.10.2'
ARCHZBM_NET_GATEWAY_IP='10.10.10.1'
ARCHZBM_NET_NETMASK='255.255.255.0'
ARCHZBM_NET_DEVICE='eth0'
ARCHZBM_NET_AUTOCONF='none'
```
Note that in this example `ARCHZBM_NET_SERVER_IP` and `ARCHZBM_NET_HOSTNAME` are left unassigned.
This add the following `ip=` instruction to your Kernel Command Line:
```
ip=10.10.10.2::10.10.10.1:255.255.255.0::eth0:none
```
#### SSH
If you want networking indicated by the fact that at least one of the `ARCHZBM_NET_*` variables is set or one of the `ARCHZBM_SSH_*` vars we assume that you want an SSH daemon as well. This comes in the form of a `dropbear` daemon with minimal configurability. Use the following variables to define Dropbear's behavior.
```
ARCHZBM_SSH_PORT='22'
ARCHZBM_SSH_KEEPALIVE_INTVL='1'
ARCHZBM_SSH_AUTH_KEYS=''
```
In `ARCHZBM_SSH_PORT` you specify Dropbear's listening port, this defaults to `22` if unconfigured or set to an empty string. With `ARCHZBM_SSH_KEEPALIVE_INTVL` you define at which interval Dropbear will send keepalive messages to an SSH client through the SSH connection. This defaults to `1` as in every `1` second a keepalive message is sent.
Dropbear in this setup only supports key-based authentication, no password-based authentication. The value from `ARCHZBM_SSH_AUTH_KEYS` will be converted to a list of public SSH keys allowed to SSH into Dropbear as its default `root` user while ZFSBootMenu is running. The format of `ARCHZBM_SSH_AUTH_KEYS` is a single line where `authorized_keys` entries are split with double-commas:
```
ssh-rsa Eahajei8,,ssh-ed25519 kaeD0mas ...
```
This syntax crutch allows you to use the full range of Dropbear-supported `authorized_keys` stanzas, see [man 8 dropbear](https://man.archlinux.org/man/extra/dropbear/dropbear.8.en) for what's available. Whether or not this is useful to you is another topic :) At least the functionality for stanzas is there by separating values in `ARCHZBM_SSH_AUTH_KEYS` with double-commas.
# Steps
The script takes the following installation steps.
@@ -498,7 +577,12 @@ arch-chroot /mnt /bin/bash
When done exit `chroot` and cleanly remove your work:
```
# UEFI system ...
umount /mnt/efi
# ... or legacy BIOS system
umount /mnt/boot/syslinux
zfs umount -a
zpool export zpool
```

156
setup.sh
View File

@@ -468,6 +468,29 @@ function in_file_in_array_insert_n_before_m () {
arg_array="${2:?}"
arg_string="${3:?}"
arg_precede="${4:?}"
# Look for a line that contains in this order
# - String "${arg_array}" and a equals sign (=), assign capture group \1
# - Followed by as few characters as possible followed by either an
# opening parenthesis or a space character, assign capture group \2
# - String "${arg_precede}", capture as capture group \3
# - Followed by either a closing parenthesis or a space which are then
# followed by as many non-line break characters as possible, capture
# as capture group \2
#
# For following example text we're assuming that:
# - "${arg_array}" equals 'HOOKS'
# - "${arg_precede}" equals 'filesystems'
# - "${arg_string}" equals 'zfs'
#
# This finds a 'HOOKS=' array definition that contains the string
# 'filesystems' either at the beginning of the 'HOOKS=(...)' opening
# parenthesis, at the very end or somewhere in the middle where it may
# be preceded or followed by one or more space characters. It saves
# 'HOOKS=', it saves whatever precedes 'filesystems' and 'filesystems'
# itself plus whatever comes after 'filesystems' until end of line. It
# lastly inserts 'zfs' and a space character right in front of
# 'filesystems'.
sed -ri \
-e 's'$'\x1''('"${arg_array}"'=)(.*?[( ])('"${arg_precede}"')([) ][^\r\n\f]*)'$'\x1''\1\2'"${arg_string}"' \3\4'$'\x1''g' \
"${arg_file}"
@@ -478,6 +501,21 @@ function in_file_in_array_insert_n_at_the_end () {
arg_file="${1:?}"
arg_array="${2:?}"
arg_string="${3:?}"
# Look for end of array, insert "${arg_string}" right before
#
# For following example text we're assuming that:
# - "${arg_array}" equals 'HOOKS'
# - "${arg_string}" equals 'net'
#
# This:
# - Finds a 'HOOKS=' array definition, saves it as \1
# - Finds as many non-closing parenthesis characters as possible, so all
# characters until just before the final ')' character of the line and
# saves this as \2
# - Finds the closing parenthesis character plus all non-line break
# characters until end of line that come after it and saves this as \3
# - Adds a space character and 'net' after \2
sed -ri \
-e 's'$'\x1''('"${arg_array}"'=)([^)]*)(\)[^\r\n\f]*)'$'\x1''\1\2 '"${arg_string}"'\3'$'\x1''g' \
"${arg_file}"
@@ -488,6 +526,23 @@ function in_file_in_array_remove_n () {
arg_file="${1:?}"
arg_array="${2:?}"
arg_string="${3:?}"
# Look for any line that contains "${arg_string}", delete that string
#
# For following example text we're assuming that:
# - "${arg_array}" equals 'HOOKS'
# - "${arg_string}" equals 'fsck'
#
# First -e expression removes 'fsck' wherever it is defined as the one
# and only element of any HOOKS=(fsck) should such a line exist.
#
# Second -e expression finds string 'fsck' where it's preceded by space
# character(s) and followed by either space character(s) or a closing
# parenthesis.
#
# Third -e expression finds string 'fsck' where it's preceded by space
# character(s) or an opening parenthesis and followed space
# character(s).
sed -ri \
-e 's'$'\x1''((\()('"${arg_string}"')(\)))'$'\x1''\2\4'$'\x1''g' \
-e 's'$'\x1''('"${arg_array}"'=.*?)([[:space:]]+'"${arg_string}"')([[:space:]]+|\))'$'\x1''\1\3'$'\x1''g' \
@@ -741,7 +796,11 @@ EOF
fi
# Up here maybe 'ro quiet' instead of 'ro'. This is ZFSBootMenu's kernel
# command line.
# command line. In MBR/Syslinux/extlinux mode /it/ must pass arguments to
# ZFSBootMenu's kernel so check /boot/syslinux/syslinux.cfg for how we start
# ZFSBootMenu in this mode. The .Kernel.CommandLine in
# /etc/zfsbootmenu/config.yaml is irrelevant for us in MBR/Syslinux/extlinux
# mode.
# Assign cmdline for final kernel start. This is our Arch Linx kernel
# command line.
@@ -751,6 +810,8 @@ EOF
function get_dropbear_hooks () {
mkdir -p '/opt/git/quico.space/quico-os-setup/mkinitcpio-dropbear-pacman-hook/branches/main'
git -C '/opt/git/quico.space/quico-os-setup/mkinitcpio-dropbear-pacman-hook/branches/main' clone 'https://quico.space/quico-os-setup/mkinitcpio-dropbear-pacman-hook.git' .
ln -s '/opt/git/quico.space/quico-os-setup/mkinitcpio-dropbear-pacman-hook/branches/main/pacman-mkinitcpio-dropbear-hook.sh' '/usr/local/bin/pacman-mkinitcpio-dropbear-hook'
ln -s '/opt/git/quico.space/quico-os-setup/mkinitcpio-dropbear-pacman-hook/branches/main/pacman-mkinitcpio-dropbear-install.sh' '/usr/local/bin/pacman-mkinitcpio-dropbear-install'
ln -s '/opt/git/quico.space/quico-os-setup/mkinitcpio-dropbear-pacman-hook/branches/main/pacman-mkinitcpio-dropbear-hook.hook' '/usr/share/libalpm/hooks/pacman-mkinitcpio-dropbear-hook.hook'
ln -s '/opt/git/quico.space/quico-os-setup/mkinitcpio-dropbear-pacman-hook/branches/main/pacman-mkinitcpio-dropbear-install.hook' '/usr/share/libalpm/hooks/pacman-mkinitcpio-dropbear-install.hook'
}
@@ -771,43 +832,70 @@ function customize_dropbear_hooks () {
fi
}
function set_unique_ip_in_syslinux_kcl () {
local zbm_config default_ip
zbm_config="${1:?}"
default_ip="${2:?}"
# First -e expression removes first looks for lines that contain
# 'APPEND' plus a space character and only in those lines removes all
# occurrences of ' ip=' followed by as many non-space characters as
# possible. This removes whatever 'ip=' definition already was present.
#
# Second -e expression similarly looks for lines that contain 'APPEND'
# plus a space character then at their end inserts a space plus our
# desired new 'ip=' definition. This puts in place the 'ip=' we want.
sed -ri \
-e \\$'\x1''APPEND '$'\x1 s'$'\x1'' ip=([^[:space:]]*)'$'\x1'''$'\x1''gi' \
-e \\$'\x1''APPEND '$'\x1 s'$'\x1''$'$'\x1'' '"${default_ip}"''$'\x1''gi' \
"${zbm_config}"
}
function ensure_ip_in_kcl () {
local zbm_config kcl_length kcl_string default_ip ip_addr_found new_kcl first_kcl_elem
local -a kcl
paru_install 'go-yq'
zbm_config='/etc/zfsbootmenu/config.yaml'
kcl_length="$(yq '.Kernel.CommandLine | length' "${zbm_config}")"
if [[ "${kcl_length}" -eq '0' ]]; then
>&3 printf -- '%s\n' \
'No .Kernel.CommandLine YAML element with content found in '"${zbm_config}"'. Exiting ...'
exit 77
else
kcl_string="$(yq '.Kernel.CommandLine' "${zbm_config}")"
fi
local default_ip
default_ip='ip='"${ARCHZBM_NET_CLIENT_IP}"':'"${ARCHZBM_NET_SERVER_IP}"':'"${ARCHZBM_NET_GATEWAY_IP}"':'"${ARCHZBM_NET_NETMASK}"':'"${ARCHZBM_NET_HOSTNAME}"':'"${ARCHZBM_NET_DEVICE}"':'"${ARCHZBM_NET_AUTOCONF}"
mapfile -t kcl < <(<<<"${kcl_string}" tr ' ' '\n' | sed '/^$/d')
for kcl_elem in "${!kcl[@]}"; do
if grep -Piq -- 'ip=' <<<"${kcl[$kcl_elem]}"; then
ip_addr_found='true'
kcl["${kcl_elem}"]="${default_ip}"
fi
done
if [[ ! "${ip_addr_found}" ]]; then
kcl+=("${default_ip}")
fi
new_kcl=''
first_kcl_elem='true'
for kcl_elem in "${kcl[@]}"; do
if [[ ! "${first_kcl_elem}" ]]; then
new_kcl+=' '"${kcl_elem}"
if [[ "${part_schema}" = 'gpt' ]]; then
local zbm_config kcl_length kcl_string ip_addr_found new_kcl first_kcl_elem
local -a kcl
paru_install 'go-yq'
zbm_config='/etc/zfsbootmenu/config.yaml'
kcl_length="$(yq '.Kernel.CommandLine | length' "${zbm_config}")"
if [[ "${kcl_length}" -eq '0' ]]; then
>&3 printf -- '%s\n' \
'No .Kernel.CommandLine YAML element with content found in '"${zbm_config}"'. Exiting ...'
exit 77
else
new_kcl+="${kcl_elem}"
unset -v first_kcl_elem
kcl_string="$(yq '.Kernel.CommandLine' "${zbm_config}")"
fi
done
yq -i '.Kernel.CommandLine = "'"${new_kcl}"'"' "${zbm_config}"
mapfile -t kcl < <(<<<"${kcl_string}" tr ' ' '\n' | sed '/^$/d')
for kcl_elem in "${!kcl[@]}"; do
if grep -Piq -- 'ip=' <<<"${kcl[$kcl_elem]}"; then
ip_addr_found='true'
kcl["${kcl_elem}"]="${default_ip}"
fi
done
if [[ ! "${ip_addr_found}" ]]; then
kcl+=("${default_ip}")
fi
new_kcl=''
first_kcl_elem='true'
for kcl_elem in "${kcl[@]}"; do
if [[ ! "${first_kcl_elem}" ]]; then
new_kcl+=' '"${kcl_elem}"
else
new_kcl+="${kcl_elem}"
unset -v first_kcl_elem
fi
done
yq -i '.Kernel.CommandLine = "'"${new_kcl}"'"' "${zbm_config}"
else
local zbm_config
zbm_config='/boot/syslinux/syslinux.cfg'
set_unique_ip_in_syslinux_kcl "${zbm_config}" "${default_ip}"
fi
}
function set_pub_keys () {