Last active
January 23, 2023 03:25
-
-
Save jose-pr/a59ce29b4455e8539f604cd922fc118d to your computer and use it in GitHub Desktop.
Truenas Install mods on install for zfsbootmenu with encryption in zpool
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/sh | |
# vim: noexpandtab ts=8 sw=4 softtabstop=4 | |
# Setup a semi-sane environment | |
PATH=/sbin:/bin:/usr/sbin:/usr/bin | |
export PATH | |
HOME=/root | |
export HOME | |
TERM=${TERM:-xterm} | |
export TERM | |
AVATAR_PROJECT="TrueNAS" | |
if [ -e "/etc/version" ]; then | |
AVATAR_VERSION=$(cat /etc/version) | |
fi | |
# Boot Pool | |
BOOT_POOL="boot-pool" | |
NEW_BOOT_POOL="boot-pool" | |
PASS="bootpool" | |
INSTALL_SCRIPT_ROOT="/remote/scale-build" | |
# Constants for base 10 and base 2 units | |
: ${kB:=$((1000))} ${kiB:=$((1024))}; readonly kB kiB | |
: ${MB:=$((1000 * kB))} ${MiB:=$((1024 * kiB))}; readonly MB MiB | |
: ${GB:=$((1000 * MB))} ${GiB:=$((1024 * MiB))}; readonly GB GiB | |
: ${TB:=$((1000 * GB))} ${TiB:=$((1024 * GiB))}; readonly TB TiB | |
# The old pre-install checks did several things | |
# 1: Don't allow going from FreeNAS to TrueNAS or vice versa | |
# 2: Don't allow downgrading. (Not sure we can do that now.) | |
# 3: Check memory size and cpu speed. | |
# This does memory size only for now. | |
pre_install_check() | |
{ | |
# We need at least 8 GB of RAM | |
# minus 1 GB to allow for reserved memory | |
local minmem=$((7 * GiB)) | |
local kbsize=$(awk '/MemTotal/ { print $2 }' /proc/meminfo) | |
local memsize=$((kbsize * kiB)) | |
if [ ${memsize} -lt ${minmem} ]; then | |
dialog --clear --title "${AVATAR_PROJECT}" --defaultno \ | |
--yesno "This computer has less than the recommended 8 GB of RAM.\n\nOperation without enough RAM is not recommended. Continue anyway?" 7 74 || return 1 | |
fi | |
return 0 | |
} | |
wait_keypress() | |
{ | |
local _tmp | |
read -p "Press ENTER to continue." _tmp | |
} | |
# Constant media size threshold for allowing swap partitions. | |
: ${MIN_SWAPSAFE_MEDIASIZE:=$((60 * GB))}; readonly MIN_SWAPSAFE_MEDIASIZE | |
# Check if it is safe to create swap partitions on the given disks. | |
# | |
# The result can be forced by setting SWAP_IS_SAFE in the environment to either | |
# "YES" or "NO". | |
# | |
# Sets SWAP_IS_SAFE to "YES" if | |
# every disk in $@ is >= ${MIN_SWAPSAFE_MEDIASIZE} | |
# and none is USB | |
# and user says ok | |
# Otherwise sets SWAP_IS_SAFE to "NO". | |
# | |
# Use `is_swap_safe` to check the value of ${SWAP_IS_SAFE}. | |
check_is_swap_safe() | |
{ | |
local _disk _size | |
# We try to use the existing value for ${SWAP_IS_SAFE} if already set. | |
if [ -z "${SWAP_IS_SAFE}" ] ; then | |
# Check every disk in $@, aborting if an unsafe disk is found. | |
for _disk ; do | |
_size=$(get_disk_size /dev/${_disk}) | |
if [ ${_size} -lt ${MIN_SWAPSAFE_MEDIASIZE} ] || | |
grep -qF "1" /sys/block/${_disk}/removable ; then | |
SWAP_IS_SAFE="NO" | |
break | |
fi | |
done | |
fi | |
# Make sure we have a valid value for ${SWAP_IS_SAFE}. | |
# If unset, we didn't find an unsafe disk. | |
case "${SWAP_IS_SAFE:="YES"}" in | |
# Accept YES or NO (case-insensitive). | |
[Yy][Ee][Ss]) | |
# Confirm swap setup | |
if ! dialog --clear --title "${AVATAR_PROJECT}" \ | |
--yes-label "Create swap" --no-label "No swap" --yesno \ | |
"Create 16GB swap partition on boot devices?" \ | |
7 74 ; then | |
SWAP_IS_SAFE="NO" | |
fi | |
;; | |
[Nn][Oo]) ;; | |
# Reject other values. | |
*) echo "Ignoring invalid value for SWAP_IS_SAFE: ${SWAP_IS_SAFE}" | |
unset SWAP_IS_SAFE | |
check_is_swap_safe "$@" | |
;; | |
esac | |
export SWAP_IS_SAFE | |
} | |
# A specialized checkyesno for SWAP_IS_SAFE. | |
# Returns 0 if it is ok to set up swap on the chosen disks, otherwise 1. | |
# `check_is_swap_safe` must be called once before calling `is_swap_safe`. | |
is_swap_safe() | |
{ | |
case "${SWAP_IS_SAFE:?}" in | |
[Yy][Ee][Ss]) true;; | |
*) false;; | |
esac | |
} | |
get_physical_disks_list() | |
{ | |
local _disk | |
ls /sys/block/ | | |
# Skip inappropriate devices. | |
awk '!/^(md|dm|sr|st|loop)/' | | |
while read _disk; do | |
if [ -b /dev/${_disk} ] && | |
[ $(get_disk_size /dev/${_disk}) -gt ${MIN_ZFS_PARTITION_SIZE} ] && | |
! disk_is_mounted ${_disk}; then | |
echo ${_disk} | |
fi | |
done | |
} | |
get_partition() | |
{ | |
local _partition=$(ls /dev/$1$2 /dev/$1p$2 2>/dev/null) | |
if [ -z $_partition ]; then | |
echo CANT_FIND_$1$2_OR_$1p$2 | |
else | |
echo $_partition | |
fi | |
} | |
get_media_description() | |
{ | |
local _media | |
local _description | |
local _cap | |
local _lsblk | |
local _label | |
local _root_fstype | |
local _children | |
_media=$1 | |
if [ -n "${_media}" ]; then | |
_description=`sgdisk -p /dev/${_media} | grep "Model:" \ | |
| cut -d ' ' -f 2-` | |
if [ -z "${_description}" ]; then | |
_description="Unknown Device" | |
fi | |
# need to settle so that lsblk output is stable | |
udevadm settle | |
_lsblk=$(lsblk -fJ -o fstype,name,label "/dev/${_media}" | jq .blockdevices[0]) | |
_root_fstype=$(echo "${_lsblk}" | jq .fstype) | |
_children=$(echo "${_lsblk}" | jq .children) | |
if [ "${_root_fstype}" = "null" ] && [ "${_children}" = "null" ]; then | |
# no fs info in lsblk output | |
_label="" | |
elif [ "${_root_fstype}" != "null" ]; then | |
_label=$(echo "${_root_fstype}" | cut -c -15) | |
else | |
_label=$(echo "${_children}" | jq '[.[] | select(.fstype=="zfs_member")][0].label') | |
if [ "${_label}" = "null" ]; then | |
# disk is not ZFS, work way through a few different possibilties | |
# start with looking for ext4 partition, then xfs, finally settle | |
# on anything that has a fstype populated | |
_label=$(echo "${_children}" | jq '[.[] | select(.fstype=="ext4")][0]') | |
if [ "${_label}" = "null" ]; then | |
_label=$(echo "${_children}" | jq '[.[] | select(.fstype=="xfs")][0]') | |
fi | |
if [ "${_label}" = "null" ]; then | |
_label=$(echo "${_children}" | jq '[.[] | select(.fstype!="null")][0]') | |
fi | |
if [ "${_label}" = "null" ]; then | |
# no child partitions have fstype info | |
_label="" | |
else | |
_root_fstype=$(echo "${_label}" | jq .fstype | tr -d '"') | |
_label=$(echo "${_label}" | jq .label) | |
if [ "${_label}" = "null" ]; then | |
_label=$(echo "${_root_fstype}" | cut -c -15) | |
else | |
_label=$(echo "${_root_fstype}-${_label}" | cut -c -15) | |
fi | |
fi | |
else | |
echo "media: /dev/${_media} -- label: ${_label}" >> /log.install | |
_label=$(echo "zfs-${_label}" | cut -c -15) | |
fi | |
fi | |
_cap=`sgdisk -p /dev/${_media} | grep "Disk /dev/${_media}" | cut -d ' ' -f 5-6` | |
echo "${_description} ${_label} -- ${_cap}" | |
fi | |
} | |
disk_is_mounted() | |
{ | |
local _dev | |
for _dev | |
do | |
if mount -v | grep -qE "^/dev/${_dev}p?[0-9]+" | |
then | |
return 0 | |
fi | |
done | |
return 1 | |
} | |
new_install_verify() | |
{ | |
local _type="$1" | |
shift | |
local _upgradetype="$1" | |
shift | |
local _disks="$*" | |
local _tmpfile="/tmp/msg" | |
cat << EOD > "${_tmpfile}" | |
WARNING: | |
EOD | |
if [ "$_upgradetype" = "inplace" ] ; then | |
echo "- This will install into existing zpool on ${_disks}." >> ${_tmpfile} | |
else | |
echo "- This will erase ALL partitions and data on ${_disks}." >> ${_tmpfile} | |
fi | |
cat << EOD >> "${_tmpfile}" | |
- You can't use ${_disks} for sharing data. | |
NOTE: | |
- Installing on SATA, SAS, or NVMe flash media is recommended. | |
USB flash sticks are discouraged. | |
Proceed with the ${_type}? | |
EOD | |
_msg=`cat "${_tmpfile}"` | |
rm -f "${_tmpfile}" | |
dialog --clear --title "$AVATAR_PROJECT ${_type}" --yesno "${_msg}" 13 74 | |
[ $? -eq 0 ] || abort | |
} | |
ask_upgrade() | |
{ | |
local _disk="$1" | |
local _tmpfile="/tmp/msg" | |
cat << EOD > "${_tmpfile}" | |
Upgrading the installation will preserve your existing configuration. | |
Do you wish to perform an upgrade or a fresh installation on ${_disk}? | |
EOD | |
_msg=`cat "${_tmpfile}"` | |
rm -f "${_tmpfile}" | |
dialog --title "Upgrade this $AVATAR_PROJECT installation" --no-label "Fresh Install" --yes-label "Upgrade Install" --yesno "${_msg}" 8 74 | |
return $? | |
} | |
ask_upgrade_inplace() | |
{ | |
local _tmpfile="/tmp/msg" | |
cat << EOD > "${_tmpfile}" | |
User configuration settings and storage volumes are preserved and not affected by this step.\n\n | |
The boot device can be formatted to remove old versions, or the upgrade can be installed in a new boot environment without affecting any existing versions. | |
EOD | |
_msg=`cat "${_tmpfile}"` | |
rm -f "${_tmpfile}" | |
dialog --trim --title "Update Method Selection" --yes-label "Install in new boot environment" --no-label "Format the boot device" --yesno "${_msg}" 0 0 | |
return $? | |
} | |
ask_boot_method() | |
{ | |
# If we are not on efi, set BIOS as the default selected option | |
dlgflags="" | |
if [ "$BOOTMODE" != "UEFI" ] ; then | |
dlgflags="--defaultno" | |
fi | |
local _tmpfile="/tmp/msg" | |
cat << EOD > "${_tmpfile}" | |
$AVATAR_PROJECT can be booted in either BIOS or UEFI mode. | |
BIOS mode is recommended for legacy and enterprise hardware, | |
whereas UEFI may be required for newer consumer motherboards. | |
EOD | |
_msg=`cat "${_tmpfile}"` | |
rm -f "${_tmpfile}" | |
dialog ${dlgflags} --title "$AVATAR_PROJECT Boot Mode" --no-label "Boot via BIOS" --yes-label "Boot via UEFI" --yesno "${_msg}" 8 74 | |
return $? | |
} | |
install_loader() | |
{ | |
local _disk _disks | |
local _mnt partition_disk | |
_mnt="$1" | |
shift | |
_disks="$*" | |
# Tell GRUB we are booting from ZFS | |
echo "GRUB_CMDLINE_LINUX=\"root=ZFS=${BOOT_POOL}/ROOT/default\"" >> ${_mnt}/etc/default/grub | |
mkdir -p ${_mnt}/boot/efi | |
chroot ${_mnt} update-initramfs -c -k $(uname -r) -v | |
chroot ${_mnt} update-grub | |
for _disk in $_disks | |
do | |
echo "Stamping GPT loader on: /dev/${_disk}" | |
chroot ${_mnt} grub-install --target=i386-pc /dev/${_disk} | |
partition_disk=$(get_partition $_disk 2) | |
echo "Stamping EFI loader on: ${_disk}" | |
chroot ${_mnt} mkdosfs -F 32 -s 1 -n EFI ${partition_disk} | |
chroot ${_mnt} mount -t vfat ${partition_disk} /boot/efi | |
chroot ${_mnt} grub-install --target=x86_64-efi \ | |
--efi-directory=/boot/efi \ | |
--bootloader-id=debian --recheck --no-floppy | |
chroot ${_mnt} mkdir -p /boot/efi/EFI/boot | |
chroot ${_mnt} cp /boot/efi/EFI/debian/grubx64.efi /boot/efi/EFI/boot/bootx64.efi | |
chroot ${_mnt} umount /boot/efi | |
done | |
return 0 | |
} | |
save_serial_settings() | |
{ | |
# If the installer was booted with serial mode enabled, we should | |
# save these values to the installed system | |
dmesg|grep "Kernel command line"|grep -q "console=ttyS" | |
USESERIAL=$? | |
if [ "$USESERIAL" -ne 0 ] ; then return 0; fi | |
echo "update system_advanced set adv_serialconsole = 1;" | |
tty=$(dmesg|grep -E "ttyS[0-9] at I/O"|head -1|awk '{print $4}') | |
SERIALSPEED=$(setserial -G /dev/$tty|awk '{print $9}') | |
if [ -n "$SERIALSPEED" ] ; then | |
echo "update system_advanced set adv_serialspeed = $SERIALSPEED;" | |
fi | |
if [ -n "$tty" ] ; then | |
echo "update system_advanced set adv_serialport = '$tty';" | |
fi | |
} | |
get_disk_info() | |
{ | |
lsblk --bytes --nodeps --noheadings --output "$@" | |
} | |
get_disk_size() | |
{ | |
get_disk_info size "$@" | |
} | |
get_disk_logical_sector_size() | |
{ | |
get_disk_info log-sec "$@" | |
} | |
get_disk_part_type() | |
{ | |
get_disk_info parttype "$@" | |
} | |
create_partitions() | |
{ | |
local _disk="$1" | |
local _sector_size=$(get_disk_logical_sector_size /dev/${_disk}) | |
local _alignment_multiple=4096 | |
# Create BIOS boot partition | |
if ! sgdisk -a${_alignment_multiple} -n1:0:+1024K -t1:EF02 -A1:set:2 /dev/${_disk}; then | |
return 1 | |
fi | |
# Create EFI partition (Even if not used, allows user to switch to UEFI later) | |
if ! sgdisk -n2:0:+524288K -t2:EF00 /dev/${_disk}; then | |
return 1 | |
fi | |
mkdir -p /tmp/efi | |
mount /dev/${_disk}2 /tmp/efi | |
cp /remote/efi/* /tmp/efi -R | |
umount /tmp/efi | |
if is_swap_safe; then | |
if ! sgdisk -n4:0:+16777216K -t4:8200 /dev/${_disk}; then | |
return 1 | |
fi | |
wipefs -a -t zfs_member $(get_partition ${_disk} 4) | |
fi | |
# Create boot pool | |
if ! sgdisk -n3:0:0 -t3:BF01 /dev/${_disk}; then | |
return 1 | |
fi | |
return 0 | |
} | |
get_minimum_size_and_partition() | |
{ | |
local _min=0 | |
local _disk | |
local _size | |
for _disk | |
do | |
if ! create_partitions ${_disk} 1>&2; then | |
echo "Could not do anything with ${_disk}, skipping" 1>&2 | |
continue | |
fi | |
_size=$(get_disk_size $(get_partition ${_disk} 3)) | |
if [ ${_min} -eq 0 -o ${_size} -lt ${_min} ]; then | |
_min=${_size} | |
fi | |
done | |
echo ${_min} | |
} | |
# Minimum required space for an installation. | |
# Docs state 8 GiB is the bare minimum, but we specify 8 GB here for wiggle room. | |
# That should leave enough slop for alignment, boot partition, etc. | |
: ${MIN_ZFS_PARTITION_SIZE:=$((8 * GB))}; readonly MIN_ZFS_PARTITION_SIZE | |
partition_disks() | |
{ | |
local _disk _disks _disksparts | |
local _mirror | |
local _minsize | |
# Create and destroy existing pool (if exists) | |
zpool import -N -f ${BOOT_POOL} 2> /dev/null || true | |
zpool destroy -f ${BOOT_POOL} 2> /dev/null || true | |
_disks=$* | |
# Erase both typical metadata area. | |
for _disk in ${_disks}; do | |
sgdisk -Z /dev/${_disk} >/dev/null 2>&1 || true | |
sgdisk -Z /dev/${_disk} >/dev/null 2>&1 || true | |
done | |
check_is_swap_safe ${_disks} | |
_minsize=$(get_minimum_size_and_partition ${_disks}) | |
if [ ${_minsize} -lt ${MIN_ZFS_PARTITION_SIZE} ]; then | |
echo "Disk is too small to install ${AVATAR_PROJECT}" 1>&2 | |
return 1 | |
fi | |
_disksparts=$(for _disk in ${_disks}; do | |
echo $(get_partition ${_disk} 3) | |
done) | |
if [ $# -gt 1 ]; then | |
_mirror="mirror" | |
else | |
_mirror="" | |
fi | |
# Regardless of upgrade/fresh installation, if we are creating a new pool, it's going to be named after value of NEW_BOOT_POOL | |
BOOT_POOL=${NEW_BOOT_POOL} | |
KEYSTORE="/etc/keystore" | |
mkdir -p "$KEYSTORE" | |
echo "$PASS" > "$KEYSTORE/$BOOT_POOL.key" | |
zpool create -f -o cachefile=/tmp/zpool.cache -o ashift=12 -d \ | |
-o feature@async_destroy=enabled \ | |
-o feature@bookmarks=enabled \ | |
-o feature@embedded_data=enabled \ | |
-o feature@empty_bpobj=enabled \ | |
-o feature@enabled_txg=enabled \ | |
-o feature@extensible_dataset=enabled \ | |
-o feature@filesystem_limits=enabled \ | |
-o feature@hole_birth=enabled \ | |
-o feature@large_blocks=enabled \ | |
-o feature@lz4_compress=enabled \ | |
-o feature@spacemap_histogram=enabled \ | |
-o feature@userobj_accounting=enabled \ | |
-o feature@encryption=enabled \ | |
-O acltype=off -O canmount=off -O compression=lz4 -O devices=off -O mountpoint=none \ | |
-O normalization=formD -O relatime=on -O xattr=sa \ | |
-O encryption=on -O keylocation=file://"$KEYSTORE/$BOOT_POOL.key" -O keyformat=passphrase \ | |
${BOOT_POOL} ${_mirror} ${_disksparts} | |
zfs set compression=on ${BOOT_POOL} | |
cp "$KEYSTORE/$BOOT_POOL.key" "/tmp/$BOOT_POOL.key" | |
zfs create -o mountpoint=$KEYSTORE ${BOOT_POOL}/keystore | |
cp "/tmp/$BOOT_POOL.key" "$KEYSTORE/$BOOT_POOL.key" | |
chmod 000 "$KEYSTORE/$BOOT_POOL.key" | |
zfs umount "${BOOT_POOL}/keystore" | |
zfs create -o canmount=off ${BOOT_POOL}/ROOT | |
zfs set org.zfsbootmenu:keysource=${BOOT_POOL}/keystore ${BOOT_POOL} | |
return 0 | |
} | |
disk_is_freenas() | |
{ | |
local _disk="$1" | |
local _rv=1 | |
local _uuid _os_part _disk_data | |
mkdir -p /tmp/data_old | |
# This code is very clumsy. There | |
# should be a way to structure it such that | |
# all of the cleanup happens as we want it to. | |
_os_part=$(get_partition ${_disk} 2) # freebsd-boot (1) + zfs (2) | |
_disk_data=$(zdb -l ${_os_part}) | |
if [ $? -ne 0 ]; then | |
_os_part=$(get_partition ${_disk} 3) # bios-boot (1) + efi (2) + zfs (3) | |
_disk_data=$(zdb -l ${_os_part}) | |
if [ $? -ne 0 ]; then | |
return 1 | |
fi | |
fi | |
echo ${_disk_data} | grep -qF "name: '${BOOT_POOL}'" | |
if [ $? -eq 1 ]; then | |
echo ${_disk_data} | grep -qF "name: 'freenas-boot'" || return 1 | |
BOOT_POOL="freenas-boot" | |
fi | |
zpool import -N -f ${BOOT_POOL} || return 1 | |
# Now we want to figure out which dataset to use. | |
DS=$(zpool list -H -o bootfs ${BOOT_POOL} | head -n 1 | cut -d '/' -f 3) | |
if [ -z "$DS" ]; then | |
zpool export ${BOOT_POOL} || true | |
return 1 | |
fi | |
zfs set mountpoint=legacy ${BOOT_POOL}/ROOT/"${DS}" || return 1 | |
mount -t zfs ${BOOT_POOL}/ROOT/"${DS}" /tmp/data_old || return 1 | |
# If the active dataset doesn't have a database file, | |
# then it's not FN as far as we're concerned (the upgrade code | |
# will go badly). | |
# We also check for the Corral database directory. | |
if [ ! -f /tmp/data_old/data/freenas-v1.db -o \ | |
-d /tmp/data_old/data/freenas.db ]; then | |
umount /tmp/data_old || true | |
zpool export ${BOOT_POOL} || true | |
return 1 | |
fi | |
( | |
cd /tmp/data_old && | |
rsync -aRx \ | |
--exclude data/factory-v1.db \ | |
--exclude data/manifest.json \ | |
data \ | |
root \ | |
/tmp/data_preserved/ | |
) | |
if [ -f /tmp/data_old/bin/freebsd-version ]; then | |
( | |
cd /tmp/data_old && | |
rsync -aRx \ | |
bin/freebsd-version \ | |
/tmp/data_preserved/ | |
) | |
( | |
cd /tmp/data_old/conf/base && | |
rsync -aRx \ | |
etc/hostid \ | |
/tmp/data_preserved/ | |
) | |
_uuid=$(get_disk_parttype $(get_partition ${_disk} 1)) | |
if [ "${_uuid}" = "83bd6b9d-7f41-11dc-be0b-001560b84f0f" ]; then # FreeBSD boot | |
sgdisk -t1:EF02 /dev/${_disk} || return 1 | |
fi | |
else | |
( | |
cd /tmp/data_old && | |
rsync -aRx \ | |
etc/hostid \ | |
etc/machine-id \ | |
/tmp/data_preserved/ | |
) | |
fi | |
umount /tmp/data_old || return 1 | |
zpool export ${BOOT_POOL} | |
} | |
prompt_password() | |
{ | |
local values value password="" password1 password2 _counter _tmpfile="/tmp/pwd.$$" | |
cat << __EOF__ > /tmp/dialogconf | |
bindkey formfield TAB FORM_NEXT | |
bindkey formfield DOWN FORM_NEXT | |
bindkey formfield UP FORM_PREV | |
bindkey formbox DOWN FORM_NEXT | |
bindkey formbox TAB FORM_NEXT | |
bindkey formbox UP FORM_PREV | |
__EOF__ | |
export DIALOGRC="/tmp/dialogconf" | |
while true; do | |
dialog --insecure \ | |
--extra-button --extra-label "Do not set password" \ | |
--output-fd 3 \ | |
--visit-items \ | |
--passwordform "Enter your root password; cancel for cancelling installation" \ | |
10 70 0 \ | |
"Password:" 1 10 "" 0 30 25 50 \ | |
"Confirm Password:" 2 10 "" 2 30 25 50 \ | |
3> ${_tmpfile} | |
ret=$? | |
if [ $ret -eq 1 ]; then | |
rm -f ${_tmpfile} | |
return 1 | |
fi | |
{ read password1 ; read password2; } < ${_tmpfile} | |
rm -f ${_tmpfile} | |
if [ $ret -eq 3 ]; then | |
password="" | |
break | |
elif [ "${password1}" != "${password2}" ]; then | |
dialog --msgbox "Passwords do not match." 7 60 2> /dev/null | |
elif [ -z "${password1}" ]; then | |
dialog --msgbox "Empty password is not secure" 7 60 2> /dev/null | |
else | |
password="${password1}" | |
break | |
fi | |
done | |
rm -f ${DIALOGRC} | |
unset DIALOGRC | |
echo -n "${password}" 1>&2 | |
} | |
cleanup() | |
{ | |
zpool export -f ${BOOT_POOL} | |
zpool export -f ${NEW_BOOT_POOL} | |
} | |
abort() | |
{ | |
set +e +x | |
trap - EXIT | |
exit 1 | |
} | |
fail() | |
{ | |
local _action=${1} | |
shift | |
local _disks=${@} | |
set +x | |
read -p "The ${AVATAR_PROJECT} ${_action} on ${_disks} has failed. Press enter to continue..." junk | |
abort | |
} | |
doing_upgrade() | |
{ | |
test -d /tmp/data_preserved | |
} | |
menu_install() | |
{ | |
local _action | |
local _disklist | |
local _tmpfile | |
local _answer | |
local _cdlist | |
local _items | |
local _disk | |
local _disks="" | |
local _realdisks="" | |
local _disk_old | |
local _config_file | |
local _desc | |
local _list | |
local _msg | |
local _i | |
local _do_upgrade="" | |
local _msg | |
local _dlv | |
local _password | |
local os_part | |
local data_part | |
local whendone="" | |
local readonly CD_UPGRADE_SENTINEL="/data/cd-upgrade" | |
local readonly NEED_UPDATE_SENTINEL="/data/need-update" | |
# create a sentinel file for post-fresh-install boots | |
local readonly FIRST_INSTALL_SENTINEL="/data/first-boot" | |
local readonly TRUENAS_EULA_PENDING_SENTINEL="/data/truenas-eula-pending" | |
local readonly POOL=${BOOT_POOL} | |
_tmpfile="/tmp/answer" | |
TMPFILE=$_tmpfile | |
REALDISKS="/tmp/realdisks" | |
while getopts "U:P:X:" opt; do | |
case "${opt}" in | |
U) if ${OPTARG}; then _do_upgrade=1 ; else _do_upgrade=0; fi | |
;; | |
P) _password="${OPTARG}" | |
;; | |
X) case "${OPTARG}" in | |
reboot) whendone=reboot ;; | |
"wait") whendone=wait ;; | |
halt) whendone="shutdown now" ;; | |
*) whendone="" ;; | |
esac | |
;; | |
*) echo "Unknown option ${opt}" 1>&2 | |
;; | |
esac | |
done | |
shift $((OPTIND-1)) | |
if [ $# -gt 0 ] | |
then | |
_disks="$@" | |
INTERACTIVE=false | |
else | |
INTERACTIVE=true | |
fi | |
# Make sure we are working from a clean slate. | |
cleanup >/dev/null 2>&1 | |
if ${INTERACTIVE}; then | |
pre_install_check || return 0 | |
while [ -z "${_disks}" ]; do | |
_list="" | |
_items=0 | |
for _disk in $(get_physical_disks_list); do | |
_desc=$(get_media_description "${_disk}" | sed "s/'/'\\\''/g") | |
_list="${_list} ${_disk} '${_desc}' off" | |
_items=$((${_items} + 1)) | |
done | |
_tmpfile="/tmp/answer" | |
if [ ${_items} -eq 0 ]; then | |
# Inform the user | |
eval "dialog --title 'Choose destination media' --msgbox 'No drives available' 5 60" 2>${_tmpfile} | |
continue | |
fi | |
eval "dialog --title 'Choose destination media' \ | |
--checklist 'Install $AVATAR_PROJECT to a drive. Multiple drives can be selected to provide redundancy. Chosen drives are not available for use in the TrueNAS UI. Arrow keys highlight options, spacebar selects.' \ | |
20 60 0 ${_list}" 2>${_tmpfile} | |
if [ -f "${_tmpfile}" ]; then | |
_disks=$(eval "echo `cat "${_tmpfile}"`") | |
rm -f "${_tmpfile}" | |
fi | |
if [ -z "${_disks}" ]; then | |
dialog --msgbox "You need to select at least one disk!" 6 74 | |
continue | |
fi | |
done | |
else | |
if [ -z "${_disks}" ] || disk_is_mounted ${_disks}; then | |
abort | |
fi | |
fi | |
_action="installation" | |
_upgrade_type="format" | |
# This needs to be re-done. | |
# If we're not interactive, then we have | |
# to assume _disks is correct. | |
# If we do have more than one disk given, | |
# we should also do something if they're all | |
# freenas disks. But how do we figure out which | |
# one to use? The current code in disk_is_freenas | |
# is very, very heavy -- it actually backs up the | |
# data from a freenas installation. It also does | |
# a zpool import. | |
for _disk in ${_disks}; do | |
if disk_is_freenas ${_disk} ; then | |
if ${INTERACTIVE}; then | |
if ask_upgrade ${_disk} ; then | |
_do_upgrade=1 | |
_action="upgrade" | |
fi | |
else | |
if [ "${_do_upgrade}" != "0" ]; then | |
_do_upgrade=1 | |
_action="upgrade" | |
fi | |
fi | |
# Ask if we want to do a format or inplace upgrade | |
if ${INTERACTIVE}; then | |
if ask_upgrade_inplace ; then | |
_upgrade_type="inplace" | |
fi | |
fi | |
break | |
fi | |
done | |
# If we haven't set _do_upgrade by now, we're not | |
# doing an upgrade. | |
if [ -z "${_do_upgrade}" ]; then | |
_do_upgrade=0 | |
fi | |
_realdisks=${_disks} | |
${INTERACTIVE} && new_install_verify "${_action}" "${_upgrade_type}" ${_realdisks} | |
if ${INTERACTIVE} && [ ${_do_upgrade} -eq 0 ]; then | |
prompt_password 2> /tmp/password | |
if [ $? -eq 0 ]; then | |
_password="$(cat /tmp/password 2> /dev/null)" | |
else | |
echo "Installation cancelled" | |
exit 1 | |
fi | |
fi | |
if [ ${_do_upgrade} -eq 0 ]; then | |
# With the new partitioning, disk_is_freenas may | |
# copy /data. So if we don't need it, remove it, | |
# or else it'll do an update anyway. Oops. | |
rm -rf /tmp/data_preserved | |
fi | |
# Start critical section. | |
trap "fail ${_action} ${_realdisks}" EXIT | |
set -e | |
# set -x | |
# _disk, _image, _config_file | |
if [ "${_upgrade_type}" = "inplace" ] | |
then | |
# When doing new-style upgrades, we can keep the old zpool | |
# and instead do a new BE creation | |
zpool import -N -f ${BOOT_POOL} | |
zfs create -o canmount=off -o mountpoint=legacy ${BOOT_POOL}/grub || true | |
else | |
# Destroy existing partition table, if there is any but tolerate | |
# failure. | |
for _disk in ${_realdisks}; do | |
wipefs -a /dev/${_disk} || echo Warning: unable to wipe partition table | |
done | |
# We repartition on fresh install, or old upgrade_style | |
# This destroys all of the pool data, and | |
# ensures a clean filesystems. | |
partition_disks ${_realdisks} | |
fi | |
local OS=TrueNAS | |
# Mount update image | |
umount /mnt || true | |
mount /cdrom/TrueNAS-SCALE.update /mnt -t squashfs -o loop | |
space_required=$(cat /mnt/manifest.json | jq ".size") | |
free_space=$(zpool get -H -o value -p free "$BOOT_POOL") | |
if [ "$free_space" -lt "$space_required" ] ; then | |
${INTERACTIVE} && dialog --msgbox "Insufficient disk space available. TrueNAS requires \ | |
$(numfmt --to=iec-i --suffix=B --format="%.1f" $space_required) but only \ | |
$(numfmt --to=iec-i --suffix=B --format="%.1f" $free_space) are available" 6 74 | |
trap - EXIT | |
exit 1 | |
fi | |
local sql="$(save_serial_settings | tr '\n' ' ')" | |
local json=$(cat <<EOF | |
{"disks": [], "force_grub_install": true, "pool_name": "${BOOT_POOL}", "sql": "${sql}", "src": "/mnt"} | |
EOF | |
) | |
for _disk in ${_realdisks}; do | |
json="$(echo "$json" | jq --arg v "${_disk}" '.disks += [$v]' -)" | |
done | |
if doing_upgrade; then | |
json="$(echo "$json" | jq --arg v "/tmp/data_preserved" '.old_root = $v' -)" | |
else | |
if [ -n "${_password}" ]; then | |
json="$(echo "$json" | jq --arg v "${_password}" '.password = $v' -)" | |
fi | |
fi | |
(cd "$INSTALL_SCRIPT_ROOT" && echo "$json" | python3 -m truenas_install) | |
# TODO: Bring this back | |
if doing_upgrade; then | |
${INTERACTIVE} && dialog --msgbox "The installer has preserved your database file. | |
$AVATAR_PROJECT will migrate this file, if necessary, to the current format." 6 74 | |
fi | |
umount -f /mnt | |
# Export the pool now | |
zpool export ${BOOT_POOL} | |
# End critical section. | |
set +e | |
trap - EXIT | |
_msg="The $AVATAR_PROJECT $_action on ${_realdisks} succeeded!\n" | |
_msg="${_msg}Please reboot and remove the installation media." | |
if ${INTERACTIVE}; then | |
dialog --msgbox "$_msg" 6 74 | |
elif [ -n "${whendone}" ]; then | |
case "${whendone}" in | |
halt) shutdown now ;; | |
"wait") dialog --msgbox "$_msg" 6 74 ;; | |
esac | |
return 0 | |
fi | |
return 0 | |
} | |
menu_shell() | |
{ | |
/bin/sh | |
} | |
menu_reboot() | |
{ | |
echo "Rebooting..." | |
reboot >/dev/null | |
} | |
menu_shutdown() | |
{ | |
echo "Halting and powering down..." | |
shutdown now >/dev/null | |
} | |
report_installation() | |
{ | |
for iface in /sys/class/net/*; do | |
name=${iface##*/}; | |
if [ $name = "lo" ]; then | |
continue; | |
fi | |
udhcpc -i "${name}"; | |
done | |
hash=$(cat /etc/hostid | sha256sum | tr -d "[:space:]-"); | |
version=$(cat /etc/version); | |
usage_str=$( jq -n \ | |
--arg p "TrueNAS-SCALE" \ | |
--arg h "${hash}" \ | |
--argjson v "[{\"version\": \"$version\"}]" \ | |
'{system_hash: $h, platform: $p, "install": $v}' ) | |
echo $usage_str | curl -X POST -m 15 -d '@-' http://usage.freenas.org/submit | |
echo "Completed reporting installation" | |
} | |
main() | |
{ | |
local _tmpfile="/tmp/answer" | |
local _number | |
if [ $# -gt 0 ]; then | |
# $1 will have the device name | |
menu_install "$@" | |
exit $? | |
fi | |
# Keep ZFS happy | |
[ -f /etc/hostid ] || zgenhostid | |
depmod | |
modprobe zfs | |
if [ ! -f /tmp/usage_stats.log ]; then | |
[ -f /etc/version ] && report_installation > /tmp/usage_stats.log 2>&1 & | |
fi | |
while :; do | |
dialog --clear --title "$AVATAR_PROJECT $AVATAR_VERSION Console Setup" --menu "" 12 73 6 \ | |
"1" "Install/Upgrade" \ | |
"2" "Shell" \ | |
"3" "Reboot System" \ | |
"4" "Shutdown System" \ | |
2> "${_tmpfile}" | |
_number=`cat "${_tmpfile}"` | |
case "${_number}" in | |
1) menu_install ;; | |
2) menu_shell ;; | |
3) menu_reboot ;; | |
4) menu_shutdown ;; | |
esac | |
# Unset cached setting | |
unset SWAP_IS_SAFE | |
done | |
} | |
# Parse a config file. | |
# We don't do much in the way of error checking. | |
# Format is very simple: | |
# <opt>=<value> | |
# <value> may be a list (e.g., disk devices) | |
# The output is suitable to be used as the arguments | |
# to main(), which will directl ycall menu_install(). | |
yesno() | |
{ | |
# Output "true" or "false" depending on the argument | |
if [ $# -ne 1 ]; then | |
echo "false" | |
return 0 | |
fi | |
case "$1" in | |
[yY][eE][sS] | [tT][rR][uU][eE]) echo true ;; | |
*) echo false;; | |
esac | |
return 0 | |
} | |
getsize() | |
{ | |
# Given a size specifier, convert it to bytes. | |
# No suffix, or a suffix of "[bBcC]", means bytes; | |
# [kK] is 1024, etc. | |
if [ $# -ne 1 ]; then | |
echo 0 | |
return 0 | |
fi | |
case "$1" in | |
*[bB][cC]) expr "$1" : "^\([0-9]*\)[bB][cC]" || echo 0;; | |
*[kK]) expr $(expr "$1" : "^\([0-9]*\)[kK]") \* 1024 || echo 0;; | |
*[mM]) expr $(expr "$1" : "^\([0-9]*\)[gG]") \* 1024 \* 1024 || echo 0;; | |
*[gG]) expr $(expr "$1" : "^\([0-9]*\)[gG]") \* 1024 \* 1024 \* 1024 || echo 0;; | |
*[tT]) expr $(expr "$1" : "^\([0-9]*\)[tT]") \* 1024 \* 1024 \* 1024 \* 1024 || echo 0;; | |
*) expr "$1" : "^\([0-9]*\)$" || echo 0;; | |
esac | |
return 0 | |
} | |
main "$@" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding=utf-8 -*- | |
import contextlib | |
import itertools | |
import json | |
import logging | |
import os | |
import pathlib | |
import platform | |
import re | |
import shutil | |
import sqlite3 | |
import stat | |
import subprocess | |
import sys | |
import tempfile | |
import textwrap | |
import psutil | |
logger = logging.getLogger(__name__) | |
EFI_SYSTEM_PARTITION_GUID = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B" | |
FREEBSD_BOOT_PARTITION_GUID = "83BD6B9D-7F41-11DC-BE0B-001560B84F0F" | |
CORE_BSD_LOADER_PATH = "/boot/efi/efi/boot/BOOTx64.efi" | |
SCALE_BSD_LOADER_PATH = "/boot/efi/efi/boot/FreeBSD.efi" | |
RE_UNSQUASHFS_PROGRESS = re.compile(r"\[.+\]\s+(?P<extracted>[0-9]+)/(?P<total>[0-9]+)\s+(?P<progress>[0-9]+)%") | |
run_kw = dict(check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8", errors="ignore") | |
IS_FREEBSD = platform.system().upper() == "FREEBSD" | |
is_json_output = False | |
def write_progress(progress, message): | |
if is_json_output: | |
sys.stdout.write(json.dumps({"progress": progress, "message": message}) + "\n") | |
else: | |
sys.stdout.write(f"[{int(progress * 100)}%] {message}\n") | |
sys.stdout.flush() | |
def write_error(error, raise_=False): | |
if is_json_output: | |
sys.stdout.write(json.dumps({"error": error}) + "\n") | |
else: | |
sys.stdout.write(f"Error: {error}\n") | |
sys.stdout.flush() | |
if raise_: | |
raise Exception(error) | |
def run_command(cmd, **kwargs): | |
try: | |
return subprocess.run(cmd, **dict(run_kw, **kwargs)) | |
except subprocess.CalledProcessError as e: | |
write_error(f"Command {cmd} failed with exit code {e.returncode}: {e.stderr}") | |
raise | |
def get_partition(disk, partition): | |
paths = [f"/dev/{disk}{partition}", f"/dev/{disk}p{partition}"] | |
for path in paths: | |
if os.path.exists(path): | |
return path | |
raise Exception(f"Neither {' or '.join(paths)} exist") | |
def get_partition_guid(disk, partition): | |
return dict(map( | |
lambda s: s.split(": ", 1), | |
run_command(["sgdisk", "-i", str(partition), f"/dev/{disk}"]).stdout.splitlines(), | |
))["Partition GUID code"].split()[0] | |
def dict_factory(cursor, row): | |
d = {} | |
for idx, col in enumerate(cursor.description): | |
d[col[0]] = row[idx] | |
return d | |
def query_config_table(table, database_path, prefix=None): | |
database_path = database_path | |
conn = sqlite3.connect(database_path) | |
try: | |
conn.row_factory = dict_factory | |
c = conn.cursor() | |
try: | |
c.execute(f"SELECT * FROM {table}") | |
result = c.fetchone() | |
finally: | |
c.close() | |
finally: | |
conn.close() | |
if prefix: | |
result = {k.replace(prefix, ""): v for k, v in result.items()} | |
return result | |
def configure_serial_port(root, db_path): | |
if not os.path.exists(db_path): | |
return | |
# We would like to explicitly enable/disable serial-getty in the new BE based on db configuration | |
advanced = query_config_table("system_advanced", db_path, prefix="adv_") | |
if advanced["serialconsole"]: | |
run_command( | |
["chroot", root, "systemctl", "enable", f"serial-getty@{advanced['serialport']}.service"], check=False | |
) | |
def enable_system_user_services(root, old_root): | |
configure_serial_port(root, os.path.join(old_root, "data/freenas-v1.db")) | |
enable_user_services(root, old_root) | |
def enable_user_services(root, old_root): | |
user_services_file = os.path.join(old_root, "data/user-services.json") | |
if not os.path.exists(user_services_file): | |
return | |
with open(user_services_file, 'r') as f: | |
systemd_units = [ | |
srv for srv, enabled in json.loads(f.read()).items() if enabled | |
] | |
if systemd_units: | |
run_command(["chroot", root, "systemctl", "enable"] + systemd_units, check=False) | |
def install_grub_freebsd(input, manifest, pool_name, dataset_name, disks): | |
boot_partition_type = None | |
for disk in disks: | |
gpart_backup = run_command(["gpart", "backup", disk]).stdout.splitlines() | |
partition_table_type = gpart_backup[0].split()[0] | |
if partition_table_type == "GPT": | |
boot_partition_type_probe = gpart_backup[1].split()[1] | |
if boot_partition_type_probe not in ["bios-boot", "freebsd-boot", "efi"]: | |
write_error(f"Invalid first partition type {boot_partition_type_probe} on {disk}", raise_=True) | |
if boot_partition_type and boot_partition_type != boot_partition_type_probe: | |
write_error("Non-matching first partition types across disks", raise_=True) | |
boot_partition_type = boot_partition_type_probe | |
else: | |
write_error(f"Invalid partition table type {partition_table_type} on {disk}", raise_=True) | |
run_command(["zpool", "set", f"bootfs={dataset_name}", pool_name]) | |
for f in ["/usr/local/etc/grub.d/10_kfreebsd", "/usr/local/etc/grub.d/30_os-prober"]: | |
with contextlib.suppress(FileNotFoundError): | |
os.unlink(f) | |
os.makedirs("/usr/local/etc/default", exist_ok=True) | |
run_command(["truenas-grub.py"]) | |
cmdline = run_command(["sh", "-c", ". /usr/local/etc/default/grub; echo $GRUB_CMDLINE_LINUX"]).stdout.strip() | |
for device in input["devices"]: | |
fs_uuid = run_command(["grub-probe", "--device", f"/dev/{device}", "--target=fs_uuid"]).stdout.strip() | |
if fs_uuid: | |
break | |
else: | |
write_error(f"None of {input['devices']!r} has GRUB fs_uuid", raise_=True) | |
grub_script_path = "/usr/local/etc/grub.d/10_truenas" | |
with open(grub_script_path, "w") as f: | |
freebsd_root_dataset = [p for p in psutil.disk_partitions() if p.mountpoint == "/"][0].device | |
run_command(["zfs", "set", "truenas:12=1", freebsd_root_dataset]) | |
f.write(textwrap.dedent(f"""\ | |
#!/bin/sh | |
cat << 'EOF' | |
menuentry 'TrueNAS SCALE' --class truenas --class gnu-linux --class gnu --class os """ | |
f"""$menuentry_id_option 'gnulinux-simple-{fs_uuid}' {{ | |
load_video | |
insmod gzio | |
if [ x$grub_platform = xxen ]; then insmod xzio; insmod lzopio; fi | |
insmod part_gpt | |
insmod zfs | |
search --no-floppy --fs-uuid --set=root {fs_uuid} | |
echo 'Loading Linux {manifest['kernel_version']} ...' | |
linux /ROOT/{manifest['version']}@/boot/vmlinuz-{manifest['kernel_version']} """ | |
f"""root=ZFS={dataset_name} ro {cmdline} console=tty1 zfs_force=yes | |
echo 'Loading initial ramdisk ...' | |
initrd /ROOT/{manifest['version']}@/boot/initrd.img-{manifest['kernel_version']} | |
}} | |
menuentry 'TrueNAS CORE' --class truenas --class gnu-linux --class gnu --class os """ | |
f"""$menuentry_id_option 'gnulinux-simple-{fs_uuid}-core' {{ | |
load_video | |
insmod gzio | |
if [ x$grub_platform = xxen ]; then insmod xzio; insmod lzopio; fi | |
insmod part_gpt | |
insmod zfs | |
search --no-floppy --fs-uuid --set=root {fs_uuid} | |
echo 'Loading Linux {manifest['kernel_version']} ...' | |
linux /ROOT/{manifest['version']}@/boot/vmlinuz-{manifest['kernel_version']} """ | |
f"""root=ZFS={dataset_name} ro {cmdline} console=tty1 zfs_force=yes """ | |
f"""systemd.setenv=_BOOT_TRUENAS_CORE=1 | |
echo 'Loading initial ramdisk ...' | |
initrd /ROOT/{manifest['version']}@/boot/initrd.img-{manifest['kernel_version']} | |
}} | |
""")) | |
os.chmod(grub_script_path, 0o0755) | |
os.makedirs("/boot/grub", exist_ok=True) | |
run_command(["zfs", "destroy", "-r", f"{pool_name}/grub"], check=False) | |
run_command(["zfs", "create", "-o", "mountpoint=legacy", f"{pool_name}/grub"]) | |
run_command(["mount", "-t", "zfs", f"{pool_name}/grub", "/boot/grub"]) | |
run_command(["grub-mkconfig", "-o", "/boot/grub/grub.cfg"]) | |
for disk in disks: | |
if boot_partition_type in ["bios-boot", "freebsd-boot"]: | |
if boot_partition_type != "bios-boot": | |
run_command(["gpart", "modify", "-i", "1", "-t", "bios-boot", f"/dev/{disk}"]) | |
run_command(["grub-install", "--target=i386-pc", f"/dev/{disk}"]) | |
elif boot_partition_type == "efi": | |
os.makedirs("/boot/efi", exist_ok=True) | |
run_command(["umount", "/boot/efi"], check=False) | |
run_command(["mount", "-t", "msdosfs", get_partition(disk, 1), "/boot/efi"]) | |
try: | |
if not os.path.exists(SCALE_BSD_LOADER_PATH): | |
shutil.copyfile(CORE_BSD_LOADER_PATH, SCALE_BSD_LOADER_PATH) | |
run_command(["grub-install", "--target=x86_64-efi", "--efi-directory=/boot/efi", "--removable"]) | |
finally: | |
run_command(["umount", "/boot/efi"]) | |
def configure_system_for_zectl(boot_pool): | |
root_ds = os.path.join(boot_pool, "ROOT") | |
set_prop = IS_FREEBSD or run_command([ | |
"zfs", "get", "-H", "-o", "value", "org.zectl:bootloader", root_ds | |
]).stdout.strip() != 'grub' | |
if set_prop: | |
run_command(["zfs", "set", "org.zectl:bootloader=grub", root_ds]) | |
def main(): | |
global is_json_output | |
input = json.loads(sys.stdin.read()) | |
cleanup = input.get("cleanup", True) | |
disks = input["disks"] | |
force_grub_install = input.get("force_grub_install", False) | |
if input.get("json"): | |
is_json_output = True | |
old_root = input.get("old_root", None) | |
password = input.get("password", None) | |
pool_name = input["pool_name"] | |
sql = input.get("sql", None) | |
src = input["src"] | |
with open(os.path.join(src, "manifest.json")) as f: | |
manifest = json.load(f) | |
dataset_name = f"{pool_name}/ROOT/{manifest['version']}" | |
old_bootfs_prop = run_command(["zpool", "get", "-H", "-o", "value", "bootfs", pool_name]).stdout.strip() | |
write_progress(0, "Creating dataset") | |
existing_datasets = set(filter(None, run_command(["zfs", "list", "-H", "-o", "name"]).stdout.split("\n"))) | |
if dataset_name in existing_datasets: | |
for i in itertools.count(1): | |
probe_dataset_name = f"{dataset_name}-{i}" | |
if probe_dataset_name not in existing_datasets: | |
dataset_name = probe_dataset_name | |
break | |
run_command([ | |
"zfs", "create", | |
"-o", "mountpoint=legacy", | |
"-o", f"truenas:kernel_version={manifest['kernel_version']}", | |
"-o", "zectl:keep=False", | |
dataset_name, | |
]) | |
try: | |
root = pathlib.Path("/boot_pool/root") | |
root.mkdir(exist_ok=True, parents=True) | |
root = root.__str__() | |
# with tempfile.TemporaryDirectory() as root: | |
run_command(["mount", "-t", "zfs", dataset_name, root]) | |
try: | |
write_progress(0, "Extracting") | |
cmd = [ | |
"unsquashfs", | |
"-d", root, | |
"-f", | |
"-da", "16", | |
"-fr", "16", | |
os.path.join(src, "rootfs.squashfs"), | |
] | |
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) | |
stdout = "" | |
buffer = b"" | |
for char in iter(lambda: p.stdout.read(1), b""): | |
buffer += char | |
if char == b"\n": | |
stdout += buffer.decode("utf-8", "ignore") | |
buffer = b"" | |
if buffer and buffer[0:1] == b"\r" and buffer[-1:] == b"%": | |
if m := RE_UNSQUASHFS_PROGRESS.match(buffer[1:].decode("utf-8", "ignore")): | |
write_progress( | |
int(m.group("extracted")) / int(m.group("total")) * 0.9, | |
"Extracting", | |
) | |
buffer = b"" | |
p.wait() | |
if p.returncode != 0: | |
write_error({"error": f"unsquashfs failed with exit code {p.returncode}: {stdout}"}) | |
raise subprocess.CalledProcessError(p.returncode, cmd, stdout) | |
write_progress(0.9, "Performing post-install tasks") | |
with contextlib.suppress(FileNotFoundError): | |
# We want to remove this for fresh installation + upgrade both | |
# In this case, /etc/machine-id would be treated as the valid | |
# machine-id which it will be otherwise as well if we use | |
# systemd-machine-id-setup --print to confirm but just to be cautious | |
# we remove this as it will be generated automatically by systemd then | |
# complying with /etc/machine-id contents | |
os.unlink(f"{root}/var/lib/dbus/machine-id") | |
is_freebsd_upgrade = False | |
setup_machine_id = configure_serial = False | |
if old_root is not None: | |
if os.path.exists(f"{old_root}/bin/freebsd-version"): | |
is_freebsd_upgrade = True | |
rsync = [ | |
"etc/hostid", | |
"data", | |
"root", | |
] | |
if is_freebsd_upgrade: | |
if not IS_FREEBSD: | |
setup_machine_id = True | |
else: | |
rsync.append("etc/machine-id") | |
run_command([ | |
"rsync", "-aRx", | |
"--exclude", "data/factory-v1.db", | |
"--exclude", "data/manifest.json", | |
"--exclude", "data/sentinels", | |
] + rsync + [ | |
f"{root}/", | |
], cwd=old_root) | |
with open(f"{root}/data/need-update", "w"): | |
pass | |
if is_freebsd_upgrade: | |
with open(f"{root}/data/freebsd-to-scale-update", "w"): | |
pass | |
else: | |
enable_system_user_services(root, old_root) | |
else: | |
run_command(["cp", "/etc/hostid", f"{root}/etc/"]) | |
with open(f"{root}/data/first-boot", "w"): | |
pass | |
with open(f"{root}/data/truenas-eula-pending", "w"): | |
pass | |
setup_machine_id = configure_serial = True | |
if setup_machine_id: | |
with contextlib.suppress(FileNotFoundError): | |
os.unlink(f"{root}/etc/machine-id") | |
run_command(["systemd-machine-id-setup", f"--root={root}"]) | |
if IS_FREEBSD: | |
install_grub_freebsd(input, manifest, pool_name, dataset_name, disks) | |
else: | |
if password is not None: | |
run_command(["chroot", root, "/etc/netcli", "reset_root_pw", password]) | |
if sql is not None: | |
run_command(["chroot", root, "sqlite3", "/data/freenas-v1.db"], input=sql) | |
if configure_serial: | |
configure_serial_port(root, os.path.join(root, "data/freenas-v1.db")) | |
undo = [] | |
try: | |
run_command(["mount", "-t", "devtmpfs", "udev", f"{root}/dev"]) | |
undo.append(["umount", f"{root}/dev"]) | |
run_command(["mount", "-t", "proc", "none", f"{root}/proc"]) | |
undo.append(["umount", f"{root}/proc"]) | |
run_command(["mount", "-t", "sysfs", "none", f"{root}/sys"]) | |
undo.append(["umount", f"{root}/sys"]) | |
# Set bootfs before running update-grub | |
run_command(["zpool", "set", f"bootfs={dataset_name}", pool_name]) | |
if is_freebsd_upgrade: | |
if old_bootfs_prop != "-": | |
run_command(["zfs", "set", "truenas:12=1", old_bootfs_prop]) | |
cp = run_command([f"{root}/usr/local/bin/truenas-initrd.py", root], check=False) | |
if cp.returncode > 1: | |
raise subprocess.CalledProcessError( | |
cp.returncode, f'Failed to execute truenas-initrd: {cp.stderr}' | |
) | |
keystore = run_command([ | |
"zfs", "get", "-H", "-o", "value", "org.zfsbootmenu:keysource", pool_name | |
]).stdout.strip() | |
keystore_mnt = run_command([ | |
"zfs", "get", "-H", "-o", "value", "mountpoint", keystore | |
]).stdout.strip() | |
run_command(["chroot", root, "zfs", "mount", keystore]) | |
undo.append(["chroot", root, "zfs", "unmount", keystore]) | |
zol_conf = pathlib.Path(f"{root}/usr/share/initramfs-tools/hooks/keystore") | |
zol_conf.write_text(f"""#!/bin/sh | |
mkdir -p "${{DESTDIR}}/{keystore_mnt}" | |
cp {keystore_mnt}/{pool_name}.key "${{DESTDIR}}/{keystore_mnt}/{pool_name}.key" | |
exit 0 | |
""") | |
zol_conf.chmod(zol_conf.stat().st_mode | stat.S_IEXEC) | |
run_command(["chmod", "+x", zol_conf.__str__()]) | |
run_command(["chroot", root, "update-initramfs", "-k", "all", "-u"]) | |
if old_root is None or force_grub_install: | |
if os.path.exists("/sys/firmware/efi"): | |
run_command(["mount", "-t", "efivarfs", "efivarfs", f"{root}/sys/firmware/efi/efivars"]) | |
undo.append(["umount", f"{root}/sys/firmware/efi/efivars"]) | |
# Clean up dumps from NVRAM to prevent | |
# "failed to register the EFI boot entry: No space left on device" | |
for item in os.listdir("/sys/firmware/efi/efivars"): | |
if item.startswith("dump-"): | |
with contextlib.suppress(Exception): | |
os.unlink(os.path.join("/sys/firmware/efi/efivars", item)) | |
os.makedirs(f"{root}/boot/efi", exist_ok=True) | |
for i, disk in enumerate(disks): | |
efi_partition_number = 2 | |
format_efi_partition = False | |
if is_freebsd_upgrade: | |
first_partition_guid = get_partition_guid(disk, 1) | |
if first_partition_guid == EFI_SYSTEM_PARTITION_GUID: | |
efi_partition_number = 1 | |
format_efi_partition = False | |
if first_partition_guid == FREEBSD_BOOT_PARTITION_GUID: | |
run_command([ | |
"sgdisk", "-t1:EF02", f"/dev/{disk}", | |
]) | |
if get_partition_guid(disk, efi_partition_number) != EFI_SYSTEM_PARTITION_GUID: | |
continue | |
partition = get_partition(disk, efi_partition_number) | |
if format_efi_partition: | |
run_command(["chroot", root, "mkdosfs", "-F", "32", "-s", "1", "-n", "EFI", | |
partition]) | |
run_command(["chroot", root, "mount", "-t", "vfat", partition, "/boot/efi"]) | |
try: | |
if os.path.exists("/sys/firmware/efi"): | |
run_command(["chroot", root, "efibootmgr", "-c", | |
"-d", f"/dev/{disk}", | |
"-p", f"{efi_partition_number}", | |
"-L", f"ZFSBootMenu", | |
"-l", "\\EFI\\zfsbootmenu.EFI" | |
]) | |
finally: | |
run_command(["chroot", root, "umount", "/boot/efi"]) | |
finally: | |
for cmd in reversed(undo): | |
run_command(cmd) | |
finally: | |
run_command(["umount", root]) | |
except Exception: | |
if old_bootfs_prop != "-": | |
run_command(["zpool", "set", f"bootfs={old_bootfs_prop}", pool_name]) | |
if cleanup: | |
run_command(["zfs", "destroy", dataset_name]) | |
raise | |
run_command(["zfs", "set", "mountpoint=/", dataset_name]) | |
run_command(["zfs", "set", "canmount=noauto", dataset_name]) | |
#configure_system_for_zectl(pool_name) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment