diff --git a/sysutils/pfSense-upgrade/Makefile b/sysutils/pfSense-upgrade/Makefile index 0d9a92a9f07b..1e10e9cf1a5d 100644 --- a/sysutils/pfSense-upgrade/Makefile +++ b/sysutils/pfSense-upgrade/Makefile @@ -1,7 +1,5 @@ -# $FreeBSD$ - PORTNAME= pfSense-upgrade -PORTVERSION= 1.2.23 +PORTVERSION= 1.2.24 CATEGORIES= sysutils MASTER_SITES= # empty DISTFILES= # empty @@ -15,7 +13,9 @@ LICENSE= APACHE20 NO_MTREE= yes NO_BUILD= yes -PLIST_FILES= libexec/pfSense-upgrade \ +PLIST_FILES= libexec/install-boot.sh \ + libexec/pfSense-upgrade \ + sbin/install-boot \ sbin/pfSense-repo-setup \ sbin/pfSense-upgrade @@ -37,5 +37,9 @@ do-install: ${STAGEDIR}${PREFIX}/libexec ${INSTALL_SCRIPT} ${WRKSRC}/pfSense-upgrade.wrapper \ ${STAGEDIR}${PREFIX}/sbin/pfSense-upgrade + ${INSTALL_SCRIPT} ${WRKSRC}/install-boot \ + ${STAGEDIR}${PREFIX}/sbin + ${INSTALL_SCRIPT} ${WRKSRC}/install-boot.sh \ + ${STAGEDIR}${PREFIX}/libexec .include diff --git a/sysutils/pfSense-upgrade/files/install-boot b/sysutils/pfSense-upgrade/files/install-boot new file mode 100755 index 000000000000..6cf70a064376 --- /dev/null +++ b/sysutils/pfSense-upgrade/files/install-boot @@ -0,0 +1,414 @@ +#!/bin/sh +# +# install-boot +# +# part of pfSense (https://www.pfsense.org) +# Copyright (c) 2024-2025 Rubicon Communications, LLC (Netgate) +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +ME="$(basename $0)" +DIR="$(dirname $0)" + +UPDATE="-u" + +_echo() +{ + [ -z "${QUIET}" ] && echo "$*" +} + +die() +{ + _echo "$*" + exit 1 +} + +doit() +{ + _echo "$*" + eval "$*" +} + +# +# Utility function to make function pipeable +# +make_pipeable() +{ + local _func="${1}" + shift + + while read -r _l; do + ${_func} ${_l} "$@" + done /dev/null +} + +# +# Resolve glabel(s) of a device node +# +dev_glabels() +{ + geom LABEL status -s | \ + while read -r _name _status _dev; do + [ "${_dev}" = "${1##/dev/}" ] || continue + echo "/dev/${_name}" + done +} + +# +# Get mount point for a device/glabel node +# +get_dev_mount() +{ + local _devs="/dev/${1##/dev/}" + + # Append possible labels for the device + _devs="${_devs} $(glabel_dev "${1}")" + _devs="${_devs} $(dev_glabels "${1}")" + + # Scan mountpoints looking for one of _devs + mount -p | \ + while read -r _mdev _mmp _mmore; do + for _dev in $_devs; do + [ "${_dev}" = "${_mdev}" ] || continue + echo "${_mmp}" + return + done + done +} + +# +# Find the root device for a given part/slice device +# +root_dev() +{ + geom PART status -s | awk -v _dev="${1##/dev/}" ' + function resolve(_d) { + if (_d in _devs) { + _depth++; + return resolve(_devs[_d]) + } + if (_depth) + return "/dev/" _d + exit 1; + } + BEGIN { _depth=0; } + { _devs[$1] = $3; } + END { print resolve(_dev); } + ' 2>/dev/null +} + +# +# Filter to expand glabels to device nodes (if any) +# +glabel_devp() +{ + make_pipeable "glabel_dev" +} + +# +# Filter to resolve root device for part/slice nodes (if any) +root_devp() +{ + make_pipeable "root_dev" +} + +find_ufs_bootdevs() +{ + ufs_on_root || die "Unexpected error." + + echo "$(rootfs_path)" | glabel_devp | root_devp +} + +# +# Get a list of potential ZFS boot devices +# +find_zfs_bootdevs() +{ + local _root="$(rootfs_path)" + + zfs_on_root || die "Unexpected error." + + zpool list -HPv "${_root%%/*}" | awk ' + $1 ~ /^\/.*$/ { + if ($10 == "ONLINE") + print $1; + } + ' | glabel_devp | root_devp +} + +# +# Return a list of boot devices for the root filesystem +# +find_bootdevs() +{ + eval find_$(rootfs_type)_bootdevs || die "Unable to locate boot devices" +} + +# +# Return the partition scheme for a given device node +# +dev_scheme() +{ + gpart show "${1}" 2>/dev/null | \ + head -n 1 | \ + cut -w -f 5 | \ + tr '[:upper:]' '[:lower:]' +} + +# +# Called early to probe the platform +# +platform_detect() +{ + case "$(sysctl -nq dev.netgate.model)" in + 1100|2100) + CURBOOTMETHOD="uefi" + BOOTMETHOD="uefi" + unset UPDATE + ;; + 3100) + _echo "Unsupported platform." + exit 0 + ;; + *) + CURBOOTMETHOD="$(sysctl -nq machdep.bootmethod | tr '[:upper:]' '[:lower:]')" + BOOTMETHOD="auto" + ;; + esac + + FSTYPE="$(rootfs_type)" + + # We want to display them in ascending order, but + # process them in descending order + BOOTDEVS="$(find_bootdevs)" + BOOTDEVSR="$(find_bootdevs | sort -r)" +} + +print_key_values() +{ + local _key="${1}"; shift + printf '%s: %s\n' "${_key}" "${1}"; shift + for _value in $*; do + printf '%*s %s\n' ${#_key} "" "${_value}" + done +} + +print_sys_info() +{ + _echo "System Configuration" + + _echo + + print_key_values "Architecture" "$(uname -m)" + print_key_values "Boot Devices" ${BOOTDEVS} + print_key_values " Boot Method" "${CURBOOTMETHOD}" + print_key_values " Filesystem" "${FSTYPE}" + print_key_values " Platform" "$(sysctl -nq dev.netgate.desc)" + + _echo +} + +usage() +{ + cat << EOD >&2 +Usage: ${ME} [-hnqy] [-d destdir] + -h - This help/usage text + -n - Dry run + -q - Quiet mode (requires -y) + -y - Assume yes when asked for confirmation + -d destir - Alternate root directory for locating boot code +EOD +} + +unset DESTDIRPARAM +unset DRYRUN +unset YES +unset QUIET + +# +# Main entry point +# +main() +{ + platform_detect + + print_sys_info + + # Sanity checks + [ -z "${BOOTDEVSR}" ] && die "Unable to find boot device(s)" + [ -z "${BOOTMETHOD}" ] && die "Unable to determine boot method" + [ -z "${FSTYPE}" ] && die "Unable to determine filesystem type" + + [ -n "${DRYRUN}" ] || \ + [ -n "${QUIET}" -a -z "${YES}" ] && exit + + if [ -z "${YES}" ]; then + read -p "Proced with updating boot code? [y/N]: " opt + case "$opt" in + y|Y) + ;; + *) + die "Aborted." + ;; + esac + fi + + # Do the update + _echo + _echo "Updating boot code..." + _echo + for _dev in ${BOOTDEVSR}; do + local _scheme="$(dev_scheme "${_dev}")" + install_boot -b ${BOOTMETHOD} ${DESTDIRPARAM} -f ${FSTYPE} -s ${_scheme} ${UPDATE} ${_dev##/dev/} || \ + die "Unable to update boot code on ${_dev}" + _echo + unset _scheme + done + + _echo "Done." + exit 0 +} + +while getopts "d:hnqy" opt; do + case "$opt" in + d) + [ -d "${OPTARG}" ] && DESTDIRPARAM="-d ${OPTARG}" || \ + die "${OPTARG} is not a valid directory path" + ;; + h) + usage + exit 0 + ;; + n) + DRYRUN=1 + ;; + q) + QUIET=1 + ;; + y) + YES=1 + ;; + *) + usage + exit 1 + ;; + esac +done + +main diff --git a/sysutils/pfSense-upgrade/files/install-boot.sh b/sysutils/pfSense-upgrade/files/install-boot.sh new file mode 100755 index 000000000000..5a619af881b0 --- /dev/null +++ b/sysutils/pfSense-upgrade/files/install-boot.sh @@ -0,0 +1,578 @@ +#!/bin/sh + +# +# Installs/updates the necessary boot blocks for the desired boot environment +# +# Lightly tested.. Intended to be installed, but until it matures, it will just +# be a boot tool for regression testing. + +# insert code here to guess what you have -- yikes! + +# Minimum size of FAT filesystems, in KB. +fat32min=33292 +fat16min=2100 + +PRODUCT="pfSense" +PRODUCT="${PRODUCT}+" + +die() { + echo "$*" + exit 1 +} + +doit() { + echo "$*" + eval "$*" +} + +# +# Translate glabel to dev node +# +glabel_dev() +{ + geom label status -s | awk -v _dev="${1##/dev/}" ' + BEGIN { _found=0; } + { _devs[$1]=$3; } + END { + if (_dev in _devs) { + _found=1 + _dev=_devs[_dev] + } + print "/dev/" _dev + if (!_found) + exit 1 + } + ' 2>/dev/null +} + +# +# Resolve glabel(s) of a device node +# +dev_glabels() +{ + glabel status -s | \ + while read -r _label _status _dev; do + [ "${_dev}" = "${1##/dev/}" ] || continue + echo "/dev/${_label}" + done +} + +# +# Get mount point for a device/glabel node +# +get_dev_mount() { + local _devs="/dev/${1##/dev/}" + + # Append possible labels for the device + _devs="${_devs} $(glabel_dev "${1}")" + _devs="${_devs} $(dev_glabels "${1}")" + + # Scan mountpoints looking for one of _devs + mount -p | \ + while read -r _mdev _mmp _mmore; do + for _dev in $_devs; do + [ "${_dev}" = "${_mdev}" ] || continue + echo "${_mmp}" + return + done + done +} + +find_part() { + dev=$1 + part=$2 + + gpart show $dev | tail +2 | awk '$4 == "'$part'" { print $3; exit; }' +} + +find_part_dev() { + dev=$1 + part=$2 + + gpart show -p $dev | tail +2 | awk '$4 == "'$part'" { print $3; exit; }' +} + + + +get_uefi_bootname() { + case ${TARGET:-$(uname -m)} in + amd64) echo bootx64 ;; + arm64) echo bootaa64 ;; + i386) echo bootia32 ;; + arm) echo bootarm ;; + riscv) echo bootriscv64 ;; + *) die "machine type $(uname -m) doesn't support UEFI" ;; + esac +} + +make_esp_file() { + local file sizekb loader device stagedir fatbits efibootname + + file=$1 + sizekb=$2 + loader=$3 + + if [ "$sizekb" -ge "$fat32min" ]; then + fatbits=32 + elif [ "$sizekb" -ge "$fat16min" ]; then + fatbits=16 + else + fatbits=12 + fi + + stagedir=$(mktemp -d /tmp/stand-test.XXXXXX) + mkdir -p "${stagedir}/EFI/BOOT" + efibootname=$(get_uefi_bootname) + cp "${loader}" "${stagedir}/EFI/BOOT/${efibootname}.efi" + makefs -t msdos \ + -o fat_type=${fatbits} \ + -o sectors_per_cluster=1 \ + -o volume_label=EFISYS \ + -s ${sizekb}k \ + "${file}" "${stagedir}" + + rm -rf "${stagedir}" +} + +make_esp_device() { + local dev file mntpt fstype efibootname kbfree loadersize efibootfile + local isboot1 existingbootentryloaderfile bootorder bootentry label + + # ESP device node + dev=$1 + file=$2 + + # See if we're using an existing (formatted) ESP + if [ "$(fstyp "${dev}")" != "msdosfs" ]; then + newfs_msdos -F 32 -c 1 -L EFISYS "${dev}" >/dev/null 2>&1 + fi + + mntpt="$(get_dev_mount "${dev}")" + if [ -n "${mntpt}" ]; then + umount "${mntpt}" + fi + + mntpt="$(mktemp -d /tmp/stand-test.XXXXXX)" + mount -t msdosfs "${dev}" "${mntpt}" + if [ $? -ne 0 ]; then + die "Failed to mount ${dev} as an msdosfs filesystem" + fi + + echo "ESP ${dev} mounted on ${mntpt}" + + efibootname=$(get_uefi_bootname) + kbfree=$(df -k "${mntpt}" | tail -1 | cut -w -f 4) + loadersize=$(stat -f %z "${file}") + loadersize=$((loadersize / 1024)) + + # Check if /EFI/BOOT/BOOTxx.EFI is the FreeBSD boot1.efi + # If it is, remove it to avoid leaving stale files around + efibootfile="${mntpt}/efi/boot/${efibootname}.efi" + if [ -f "${efibootfile}" ]; then + isboot1=$(strings "${efibootfile}" | grep "FreeBSD EFI boot block") + + if [ -n "${isboot1}" ] && [ "$kbfree" -lt "${loadersize}" ]; then + echo "Only ${kbfree}KB space remaining: removing old FreeBSD boot1.efi file /efi/boot/${efibootname}.efi" + rm "${efibootfile}" + rmdir "${mntpt}/efi/boot" + else + echo "${kbfree}KB space remaining on ESP: renaming old ${efibootname}.efi file /efi/boot/${efibootname}.efi /efi/boot/${efibootname}-old.efi" + mv "${efibootfile}" "${mntpt}/efi/boot/${efibootname}-old.efi" + fi + fi + + if [ ! -f "${mntpt}/efi/freebsd/loader.efi" ] && [ "$kbfree" -lt "$loadersize" ]; then + umount "${mntpt}" + rmdir "${mntpt}" + echo "Failed to update the EFI System Partition ${dev}" + echo "Insufficient space remaining for ${file}" + echo "Run e.g \"mount -t msdosfs ${dev} /mnt\" to inspect it for files that can be removed." + die + fi + + mkdir -p "${mntpt}/efi/freebsd" + + # Keep a copy of the existing loader.efi in case there's a problem with the new one + if [ -f "${mntpt}/efi/freebsd/loader.efi" ] && [ "$kbfree" -gt "$((loadersize * 2))" ]; then + echo "${kbfree}KB space remaining on ESP: renaming old loader.efi file /etc/freebsd/loader.efi /etc/freebsd/loader-old.efi" + cp "${mntpt}/efi/freebsd/loader.efi" "${mntpt}/efi/freebsd/loader-old.efi" + fi + + echo "Copying loader.efi to /EFI/freebsd on ESP" + cp "${file}" "${mntpt}/efi/freebsd/loader.efi" + + # Make sure we can access efi variables + efibootmgr > /dev/null + if [ $? -eq 0 ] && [ -n "${updatesystem}" ]; then + label="${PRODUCT} (${dev##/dev/})" + if ! efibootmgr | grep -q "${label}"; then + echo "Creating UEFI boot entry for FreeBSD" + efibootmgr --create --label "${label}" --loader "${mntpt}/efi/freebsd/loader.efi" > /dev/null + if [ $? -ne 0 ]; then + echo "Failed to create new boot entry" + else + # When creating new entries, efibootmgr doesn't mark them active, so we need to + # do so. It doesn't make it easy to find which entry it just added, so rely on + # the fact that it places the new entry first in BootOrder. + bootorder=$(efivar --name 8be4df61-93ca-11d2-aa0d-00e098032b8c-BootOrder --print --no-name --hex | head -1) + bootentry=$(echo "${bootorder}" | cut -w -f 3)$(echo "${bootorder}" | cut -w -f 2) + echo "Marking UEFI boot entry ${bootentry} active" + efibootmgr --activate -b "${bootentry}" > /dev/null + fi + else + echo "Existing UEFI FreeBSD boot entry found: not creating a new one" + fi + fi + + # Configure for booting from removable media + if [ ! -d "${mntpt}/efi/boot" ]; then + mkdir -p "${mntpt}/efi/boot" + fi + + echo "Copying ${efibootname}.efi to /efi/boot on ESP" + cp "${file}" "${mntpt}/efi/boot/${efibootname}.efi" + + echo "Unmounting and cleaning up temporary mount point" + umount "${mntpt}" + rmdir "${mntpt}" + + mount -a + + echo "Finished updating ESP" +} + +make_esp() { + local file loaderfile + + file=$1 + loaderfile=$2 + + if [ -f "$file" ]; then + make_esp_file ${file} ${fat32min} ${loaderfile} + else + make_esp_device ${file} ${loaderfile} + fi +} + +make_esp_mbr() { + dev=$1 + dst=$2 + + part=$(find_part_dev $dev "!239") + if [ -z "$part" ] ; then + part=$(find_part_dev $dev "efi") + if [ -z "$part" ] ; then + echo -n "No ESP slice found..." + [ -z "${AUTO}" ] && die "aborting." + echo "skipping." + return + fi + fi + make_esp /dev/${part} ${dst}/boot/loader.efi +} + +make_esp_gpt() { + dev=$1 + dst=$2 + + part=$(find_part_dev $dev "efi") + if [ -z "$part" ] ; then + echo -n "No ESP partition found..." + [ -z "${AUTO}" ] && die "aborting." + echo "skipping." + return + fi + make_esp /dev/${part} ${dst}/boot/loader.efi +} + +boot_nogeli_gpt_ufs_legacy() { + dev=$1 + dst=$2 + + idx=$(find_part $dev "freebsd-boot") + if [ -z "$idx" ] ; then + echo -n "No freebsd-boot partition found..." + [ -z "${AUTO}" ] && die "aborting." + echo "skipping." + return + fi + doit gpart bootcode -b ${gpt0} -p ${gpt2} -i $idx $dev +} + +boot_nogeli_gpt_ufs_uefi() { + make_esp_gpt $1 $2 +} + +boot_nogeli_gpt_ufs_both() { + boot_nogeli_gpt_ufs_legacy $1 $2 $3 + boot_nogeli_gpt_ufs_uefi $1 $2 $3 +} + +boot_nogeli_gpt_zfs_legacy() { + dev=$1 + dst=$2 + + idx=$(find_part $dev "freebsd-boot") + if [ -z "$idx" ] ; then + echo -n "No freebsd-boot partition found..." + [ -z "${AUTO}" ] && die "aborting." + echo "skipping." + return + fi + doit gpart bootcode -b ${gpt0} -p ${gptzfs2} -i $idx $dev +} + +boot_nogeli_gpt_zfs_uefi() { + make_esp_gpt $1 $2 +} + +boot_nogeli_gpt_zfs_both() { + boot_nogeli_gpt_zfs_legacy $1 $2 $3 + boot_nogeli_gpt_zfs_uefi $1 $2 $3 +} + +boot_nogeli_mbr_ufs_legacy() { + dev=$1 + dst=$2 + + doit gpart bootcode -b ${mbr0} ${dev} + part=$(find_part_dev $dev "freebsd") + if [ -z "$part" ] ; then + echo -n "No freebsd slice found..." + [ -z "${AUTO}" ] && die "aborting." + echo "skipping." + return + fi + doit gpart bootcode -b ${mbr2} ${part} +} + +boot_nogeli_mbr_ufs_uefi() { + make_esp_mbr $1 $2 +} + +boot_nogeli_mbr_ufs_both() { + boot_nogeli_mbr_ufs_legacy $1 $2 $3 + boot_nogeli_mbr_ufs_uefi $1 $2 $3 +} + +dev_pool() +{ + zpool list -HPv | awk -v _dev="${1}" ' + BEGIN { + _pool=""; + _prevdev=0; + } + $1 == _dev { + if (_pool) + print _pool; + exit + } + $1 !~ /^\/.*$/ { + if (!_pool || _prevdev) { + _pool=$1; + _prevdev=0; + } + } + $1 ~ /^\/.*$/ { + if (_pool) + _prevdev=1 + } + ' 2>/dev/null +} + +boot_nogeli_mbr_zfs_legacy() { + dev=$1 + dst=$2 + + # search to find the BSD slice + part1=$(find_part_dev $dev "freebsd") + if [ -z "$part1" ] ; then + echo -n "No BSD slice found..." + [ -z "${AUTO}" ] && die "aborting." + echo "skipping." + return + fi + + # search to find bootpool vdev + part2=$(find_part_dev ${part1} "freebsd-zfs") + if [ -z "$part2" ] ; then + echo -n "No freebsd-zfs slice found" + [ -z "${AUTO}" ] && die "aborting." + echo "skipping." + return + fi + + reimport=1 + if ! zpool status bootpool 1>/dev/null 2>&1; then + echo "Importing bootpool" + doit zpool import -f bootpool 1>/dev/null 2>&1 + unset reimport + fi + + mbr_tmp="$(mktemp -q /tmp/mbr.XXXXX)" + zfsboot_tmp="$(mktemp -q /tmp/zfsboot.XXXXX)" + zfsboot1_tmp="$(mktemp -q /tmp/zfsboot1.XXXXX)" + + doit cp "${mbr0}" "${mbr_tmp}" + doit cp "${zfsboot0}" "${zfsboot_tmp}" + + echo "Exporting bootpool" + doit zpool export -f bootpool 1>/dev/null 2>&1 + + # search to find the freebsd-zfs partition within the slice + # Or just assume it is 'a' because it has to be since it fails otherwise + doit gpart bootcode -b ${mbr_tmp} ${dev} + dd if="${zfsboot_tmp}" of="${zfsboot1_tmp}" count=1 + doit gpart bootcode -b "${zfsboot1_tmp}" ${part1} # Put boot1 into the start of part + sysctl kern.geom.debugflags=0x10 # Put boot2 into ZFS boot slot + doit dd if="${zfsboot_tmp}" of=/dev/${part2} skip=1 seek=1024 + sysctl kern.geom.debugflags=0x0 + + if [ -n "${reimport}" ]; then + echo "Reimporting bootpool" + doit zpool import -f bootpool + fi + + doit rm -f ${mbr_tmp} ${zfsboot_tmp} ${zfsboot1_tmp} +} + +boot_nogeli_mbr_zfs_uefi() { + make_esp_mbr $1 $2 +} + +boot_nogeli_mbr_zfs_both() { + boot_nogeli_mbr_zfs_legacy $1 $2 $3 + boot_nogeli_mbr_zfs_uefi $1 $2 $3 +} + +boot_geli_gpt_ufs_legacy() { + boot_nogeli_gpt_ufs_legacy $1 $2 $3 +} + +boot_geli_gpt_ufs_uefi() { + boot_nogeli_gpt_ufs_uefi $1 $2 $3 +} + +boot_geli_gpt_ufs_both() { + boot_nogeli_gpt_ufs_both $1 $2 $3 +} + +boot_geli_gpt_zfs_legacy() { + boot_nogeli_gpt_zfs_legacy $1 $2 $3 +} + +boot_geli_gpt_zfs_uefi() { + boot_nogeli_gpt_zfs_uefi $1 $2 $3 +} + +boot_geli_gpt_zfs_both() { + boot_nogeli_gpt_zfs_both $1 $2 $3 +} + +# GELI+MBR is not a valid configuration +boot_geli_mbr_ufs_legacy() { + exit 1 +} + +boot_geli_mbr_ufs_uefi() { + exit 1 +} + +boot_geli_mbr_ufs_both() { + exit 1 +} + +boot_geli_mbr_zfs_legacy() { + exit 1 +} + +boot_geli_mbr_zfs_uefi() { + exit 1 +} + +boot_geli_mbr_zfs_both() { + exit 1 +} + +usage() { + printf 'Usage: %s [-h] [-b bios] [-d destdir] -f fs [-g geli] [-o optargs] -s scheme \n' "$0" + printf 'Options:\n' + printf ' bootdev Device to install the boot code on\n' + printf ' -b bios Bios type: legacy, uefi, auto or both\n' + printf ' -d destdir Destination filesystem root\n' + printf ' -f fs Filesystem type: ufs or zfs\n' + printf ' -g geli geli, yes or no\n' + printf ' -h This help/usage text\n' + printf ' -u Run commands such as efibootmgr to update the\n' + printf ' currently running system\n' + printf ' -o optargs Optional arguments\n' + printf ' -s scheme mbr or gpt\n' + exit 0 +} + +# Note: we really don't support geli boot in this script yet. +geli=nogeli + +# Default bios to "both" +bios=both + +while getopts "b:d:f:g:ho:s:u" opt; do + case "$opt" in + b) + bios=${OPTARG} + if [ "${bios}" = "auto" ]; then + AUTO=1 + bios="both" + fi + ;; + d) + srcroot=${OPTARG} + ;; + f) + fs=${OPTARG} + ;; + g) + case ${OPTARG} in + [Yy][Ee][Ss]|geli) geli=geli ;; + *) geli=nogeli ;; + esac + ;; + u) + updatesystem=1 + ;; + o) + opts=${OPTARG} + ;; + s) + scheme=${OPTARG} + ;; + + ?|h) + usage + ;; + esac +done + +if [ -n "${scheme}" ] && [ -n "${fs}" ] && [ -n "${bios}" ]; then + shift $((OPTIND-1)) + dev=$1 +fi + +# For gpt, we need to install pmbr as the primary boot loader +# it knows about +gpt0=${srcroot}/boot/pmbr +gpt2=${srcroot}/boot/gptboot +gptzfs2=${srcroot}/boot/gptzfsboot + +# For MBR, we have lots of choices, but select mbr, boot0 has issues with UEFI +mbr0=${srcroot}/boot/mbr +mbr2=${srcroot}/boot/boot +zfsboot0=${srcroot}/boot/zfsboot + +# sanity check here + +# Check if we've been given arguments. If not, this script is probably being +# sourced, so we shouldn't run anything. +if [ -n "${dev}" ]; then + eval boot_${geli}_${scheme}_${fs}_${bios} $dev $srcroot $opts || echo "Invalid configuration: ${geli}-${scheme}-${fs}-${bios}" +fi diff --git a/sysutils/pfSense-upgrade/files/pfSense-upgrade b/sysutils/pfSense-upgrade/files/pfSense-upgrade index c61c309be3b3..786eaea98808 100755 --- a/sysutils/pfSense-upgrade/files/pfSense-upgrade +++ b/sysutils/pfSense-upgrade/files/pfSense-upgrade @@ -45,6 +45,8 @@ The following parameters are mutually exclusive: EOD } +BE_PREV_SUFFIX="_$(/bin/date "+%Y%m%d%H%M%S")" + _echo() { local _n="" local _out="/dev/stdout" @@ -87,14 +89,17 @@ _exec() { _stdout='' fi - [ -n "${_msg}" ] \ - && _echo -n ">>> ${_msg}... " + if [ -n "${_msg}" ]; then + LAST_EXEC_MSG="${_msg}" + _echo -n ">>> ${_msg}..." + fi + if [ -z "${_stdout}" ]; then _echo "" # Ref. https://stackoverflow.com/a/30658405 exec 4>&1 # XXX locked packages message should be printed on stderr by pkg - local _result=$( \ + LAST_EXEC_RC=$( \ { { ${_cmd} 2>&1 3>&-; printf $? 1>&3; } 4>&- \ | tee -a ${logfile} 1>&4; } 3>&1 \ ) @@ -103,14 +108,14 @@ _exec() { # Ref. https://stackoverflow.com/a/30658405 exec 4>&1 # XXX locked packages message should be printed on stderr by pkg - local _result=$( \ + LAST_EXEC_RC=$( \ { { ${_cmd} >${_stdout} 2>&1 3>&-; printf $? 1>&3; } 4>&- \ | tee -a ${logfile} 1>&4; } 3>&1 \ ) exec 4>&- fi - if [ ${_result} -eq 0 -o -n "${_ignore_result}" ]; then + if [ ${LAST_EXEC_RC} -eq 0 -o -n "${_ignore_result}" ]; then [ -n "${_stdout}" -a -n "${_msg}" ] \ && _echo "done." return 0 @@ -124,8 +129,7 @@ _exec() { } _pkg() { - pkg-static "$@" 2>/dev/null - return $? + /usr/local/sbin/pkg-static ${PKG_OPTS} "$@" 2>/dev/null } _exit() { @@ -151,8 +155,56 @@ _exit() { pkg_unlock "${pkg_prefix}*" fi + if [ -n "${BE_UPGRADE_FAILED}" ]; then + be_unmount -f "${UPGRADE_BE}" + be_destroy -o "${UPGRADE_BE}" + be_rename "${UPGRADE_BE}${BE_PREV_SUFFIX}" "${UPGRADE_BE}" + unset BE_UPGRADE_FAILED + fi + local _rc=${1:-"0"} + : ${LAST_EXEC_CMD:="unknown"} + : ${LAST_EXEC_RC:=${_rc}} + + local _notice _msg + + if [ "${LAST_EXEC_RC}" -ne 0 ]; then + case "${LAST_EXEC_CMD}" in + install-boot) + _msg="unable to upgrade boot code" + ;; + pkg) + case "${LAST_EXEC_RC}" in + 3) + _msg="out of space" + ;; + *) + ;; + esac + ;; + check_upgrade) + case "${LAST_EXEC_RC}" in + 2) #upgrade available + _msg="none" + ;; + *) + ;; + esac + ;; + *) #mute notices for anything unknown + _msg="none" + ;; + esac + + # File the notice + if [ "${_msg}" != "none" ]; then + _notice="${LAST_EXEC_CMD}: \"${_msg:-${LAST_EXEC_MSG}}\" returned error code ${LAST_EXEC_RC}" + pkg_unlock pkg + notice "${_notice}" + fi + fi + # If EVENT_PIPE is defined, GUI is calling if [ -n "${progress_socket}" ]; then local _need_reboot_str="" @@ -164,295 +216,291 @@ _exit() { exit ${_rc} } +# # Returns the current root filesystem -_be_get_rootfs() { +# +be_get_rootfs() { /sbin/mount | /usr/bin/awk '/ \/ / {print $1}' } +# # Test if root filesystem is on UFS -_be_is_root_on_ufs() { - echo "$( _be_get_rootfs )" | grep -q -m 1 -E "^/dev/" +# +be_is_ufs_root() +{ + echo "$( be_get_rootfs )" | /usr/bin/grep -q -m 1 -E "^/dev/" } +# +# Test if root filesustem is on ZFS +# +be_is_zfs_root() +{ + ! be_is_ufs_root +} + +# # Returns the pool that contains the root filesystem -_be_get_root_pool() { - _be_get_rootfs | /usr/bin/awk -F '/' '{print $1}' +# +be_get_root_pool() { + be_get_rootfs | /usr/bin/awk -F '/' '{print $1}' } +# # Returns the BE dataset -_be_get_beds() { - local _pool="$( _be_get_root_pool )" - local _beds="$( _be_get_rootfs | /usr/bin/awk -F '/' '{print $2}' )" +# +be_get_beds() +{ + local _pool="$( be_get_root_pool )" + local _beds="$( be_get_rootfs | /usr/bin/awk -F '/' '{print $2}' )" echo "${_pool}/${_beds}" } -# determine the ZFS dataset that holds a given file/folder -_be_get_dataset_of_path() { - local _path="${1}" - - /sbin/zfs list -H -t filesystem -o name -r "${_path}" 2>/dev/null -} - -# determines if a given path is held by the root dataset or a child of -# the root dataset. -_be_is_path_on_root() { - local _path="${1}" - - _be_get_dataset_of_path "${_path}" | grep -q -m 1 "$( _be_get_rootfs )" -} - -# test if root filesystem is on ZFS -_be_is_root_on_zfs() { - # just the opposite... - !(_be_is_root_on_ufs) +# +# Determine the ZFS dataset that holds a given file/folder +# +be_get_dataset_of_path() +{ + /sbin/zfs list -H -t filesystem -o name -r "${1}" 2>/dev/null } # -# test the system for BE-readiness (e.g. fails on UFS systems) +# Determines if a given path is held by the root dataset or a child of +# the root dataset. +# +be_is_path_on_root() +{ + be_get_dataset_of_path "${1}" | /usr/bin/grep -q -m 1 "$( be_get_rootfs )" +} + +# +# Test the system for boot environment support (e.g. fails on UFS systems) # # notes: -# 1. Perform a sanity check using `bectl check` -# 2. Check to see if /cf lives on the root dataset -# 3. Check to see if /var/db/pkg lives on the root dataset -# 4. TODO: add additional sanity checks? +# 1. Check if we are on Plus (UB on CE) +# 2. Check if root is on ZFS +# 3. Perform a sanity check using `bectl check` +# 4. Check to see if /cf lives on the root dataset +# 5. Check to see if /var/db/pkg lives on the root dataset # -_be_readiness_check() { - _be_is_root_on_zfs \ +be_bootenv_check() +{ + is_plus \ + && be_is_zfs_root \ && /sbin/bectl check \ - && _be_is_path_on_root "/cf" \ - && _be_is_path_on_root "/var/db/pkg" -} - -# See _be_readiness_check... -_be_not_readiness_check() { - # just the opposite... - !(_be_readiness_check) + && be_is_path_on_root "/cf" \ + && be_is_path_on_root "/var/db/pkg" } +# # Translates a BE name into a dataset path -_be_name_to_ds() { - local _name="${1}" - - echo "$( _be_get_beds )/${_name}" +# +be_get_ds() +{ + echo "$( be_get_beds )/${1}" } -# returns the base package version -_be_get_pkg_version() { - _ver="$( _pkg query %v "${product}-base" 2>/dev/null )" - if [ $? -eq 0 ]; then - echo "${_ver}" - return 0 +# +# Check if a BE is mounted, return mount point +# +be_get_mount() +{ + /sbin/bectl list -H | \ + /usr/bin/awk -v BE="${1}" \ + 'BEGIN { RET = 1 } + ($1 == BE) && ($3 ~ /^\//) { RET = 0; print $3 } + END { exit RET } + ' 2>/dev/null +} + +# +# Returns the base package version +# +be_get_base_version() +{ + _pkg query %v "${product}-base" 2>/dev/null + if [ $? -ne 0 ]; then + echo "Unknown" + return 1 fi - echo "unknown" - return 1 } +# # Returns the active BE -_be_get_active() { +# +be_active_name() +{ /sbin/bectl list -H | /usr/bin/awk 'index($2, "N") {print $1}' | /usr/bin/head -n1 } +# # Returns the active dataset -_be_get_active_ds() { - _be_name_to_ds "$( _be_get_active )" +# +be_active_ds() +{ + be_get_ds "$( be_active_name )" } -# Set a ZFS property -_be_zfs_set_property() { - local _target="${1}" - local _property="${2}" - local _value="${3}" - - /sbin/zfs set "${_property}=${_value}" "${_target}" 1>/dev/null 2>&1 - - # sanity check by reading the value back and comparing with what we expect - [ "$( _be_zfs_get_property "${_target}" "${_property}" )" = "${_value}" ] +# +# Set a ZFS user property +# +zfs_set_prop() +{ + /sbin/zfs set "${2}=${3}" "${1}" } -# Get a ZFS property -_be_zfs_get_property() { - local _target="${1}" - local _property="${2}" - - /sbin/zfs get -H -o value "${_property}" "${_target}" 2>/dev/null +# +# Unset a ZFS user property +# +zfs_unset_prop() +{ + zfs_set_prop "${1}" "${2}" "-" } +# +# Get a ZFS user property +# +zfs_get_prop() +{ + /sbin/zfs get -H -o value "${2}" "${1}" 2>/dev/null +} + +# +# Test a ZFS user property for equality +# +zfs_test_prop_eq() +{ + [ "$( zfs_get_prop "${1}" "${2}" )" = "${3}" ] +} + +# +# Same as above but takes a BE name instead +# +be_set_prop() +{ + zfs_set_prop "$( be_get_ds "${1}" )" "${2}" "${3}" +} + +# +# Same as above but takes a BE name instead +# +be_unset_prop() +{ + zfs_set_prop "$( be_get_ds "${1}" )" "${2}" +} + +# +# Same as above but takes a BE name instead +# +be_get_prop() +{ + zfs_get_prop "$( be_get_ds "${1}" )" "${2}" +} + +# +# Same as above but takes a BE name instead +# +be_test_prop_eq() +{ + zfs_test_prop_eq "$( be_get_ds "${1}" )" "${2}" "${3}" +} + +# # Check if dataset exists -_be_zfs_dataset_exists() { - local _dataset="${1}" - - /sbin/zfs list "${_dataset}" 1>/dev/null 2>&1 +# +be_zfs_ds_exists() +{ + /sbin/zfs list "${1}" 1>/dev/null 2>&1 } +# # Destroy a ZFS datset -_be_zfs_destroy() { - local _dataset="${1}" - ( !(_be_zfs_dataset_exists "${_dataset}") ) && return 0 - /sbin/zfs destroy -fr "${_dataset}" 1>/dev/null 2>&1 -} - -# Create a ZFS dataset -_be_zfs_create() { - local _source_path="${1}" - local _target_dataset="${2}" - - # create the new target dataset - /sbin/zfs create -o mountpoint="${_source_path}" "${_target_dataset}" 1>/dev/null 2>&1 -} - -# Sanity check that we have a valid rollback BE -_be_has_valid_rollback_be() { - local _active_ds="$( _be_get_active_ds )" - local _rollback_be="$( _be_zfs_get_property "${_active_ds}" pfsense:rollbackbe )" - - # Check to see if the active dataset has a rollback be dataset. - _be_zfs_dataset_exists "$( _be_name_to_ds "${_rollback_be}" )" -} - -# See _be_has_valid_rollback_be -_be_has_not_valid_rollback_be() { - # Just the opposite... - !(_be_has_valid_rollback_be) -} - -# Check if we are in the process of upgrading. -# We don't necessarily know how many times pfSense-upgrade will be called during a given -# system upgrade, so we wait and reset this flag in pfSense-rc to be sure we are done. -_be_is_upgrading() { - local _active_ds="$( _be_get_active_ds )" - local _upgrading="$( _be_zfs_get_property "${_active_ds}" pfsense:upgrading )" - - [ "${_upgrading}" = "yes" ] -} - -# Guard for _be_prune -_be_prune_guard() { - # various reasons to bail out early from _be_prune below... - _be_is_root_on_ufs || _be_readiness_check -} - -# Cleans up any BEs that might exist on incompatible systems -_be_prune() { - _be_prune_guard && return 1 - /sbin/bectl list -H | - while read -r _name _status _mp _space _creation; do - /sbin/bectl destroy -o "${_name}" 1>/dev/null 2>&1 - done - return 0 -} - -# Check if automatic update BEs should be created prior to performing a system update -_be_autobe_disabled() { - local _disableautobe="$(read_xml_tag.sh boolean system/firmware/disableautobe)" - - [ "${_disableautobe}" = "true" ] -} - -# Guard for _be_pre_upgrade -_be_pre_upgrade_guard() { - local _disableautobe="$(read_xml_tag.sh boolean system/firmware/disableautobe)" - - # various reasons to bail early from _be_prep_upgrade below... - _be_not_readiness_check || _be_autobe_disabled || _be_is_upgrading || is_ce -} - -# Prepare the system to upgrade by creating a BE before touching the -# current environment. We also set some useful ZFS user properties -# here as well for displaying in the UI. -_be_prep_upgrade() { - # short-circuit when applicable, return 0 to proceed with upgrade - _be_pre_upgrade_guard && return 0 - - local _timestamp="$( date "+%Y%m%d%H%M%S" )" - local _be_name="auto-$( _be_get_active )-${_timestamp}" - - _echo -n ">>> Creating automatic rollback boot environment..." - - /sbin/bectl create -r "${_be_name}" 1>/dev/null 2>&1 - if [ $? -eq 0 ]; then - local _active_ds="$( _be_get_active_ds )" - local _ds_name="$( _be_name_to_ds ${_be_name} )" - - # save a tiny bit of state to ensure we only run this once - _be_zfs_set_property ${_active_ds} pfsense:upgrading yes - - # cache some useful bits for easily scraping for the UI - _be_zfs_set_property ${_active_ds} pfsense:rollbackbe ${_be_name} - _be_zfs_set_property ${_ds_name} pfsense:description "Automatically created by system update" - _be_zfs_set_property ${_ds_name} pfsense:version "$( _be_get_pkg_version )" +# +be_zfs_destroy() +{ + if be_zfs_dataset_exists "${1}"; then + /sbin/zfs destroy -fr "${1}" 1>/dev/null 2>&1 + return fi - _echo " done." - - # did we actually create a valid rollback BE? - # if something went wrong during the above, this will cause the upgrade to terminate immediately... - _be_has_not_valid_rollback_be && return 1 - - # looks good return 0 } -# Guard for _be_prep_reboot -_be_prep_reboot_guard() { - # various reasons to bail early from _be_prep_reboot below... - _be_not_readiness_check +# +# Create a ZFS dataset +# +be_zfs_create() +{ + /sbin/zfs create -o mountpoint="${1}" "${2}" 1>/dev/null 2>&1 } -# Prepare the system to reboot by ensuring that we are rebooting into the -# BE that is actively being upgraded. -_be_prep_reboot() { - # short-circuit when applicable - _be_prep_reboot_guard && return 0 +# +# Sanity check that we have a valid rollback BE +# +be_has_valid_rollback_be() +{ + local _be="$( be_get_prop "$( be_active_name )" pfsense:rollbackbe )" - local _name="$( _be_get_active )" - - _echo -n ">>> Activating boot environment ${_name}..." - - /sbin/bectl activate "${_name}" 1>/dev/null 2>&1 - - _echo " done." + be_zfs_dataset_exists "$( be_get_ds "${_be}" )" } +# +# Cleans up any BEs that might exist on incompatible systems +# +be_prune() +{ + if ! be_bootenv_check; then + /sbin/bectl list -H | + while read -r _name _status _mp _space _creation; do + /sbin/bectl destroy -o "${_name}" 1>/dev/null 2>&1 + done + fi +} + +# # Prepare the system for a dataset migration -_be_pre_migration() { - local _source_path="${1}" - local _target_dataset="${2}" - local _tmp_source_path="/tmp${_source_path}.old" +# +be_pre_migration() +{ + local _tmp_source_path="/tmp${1}.old" # create a scratch location /bin/mkdir -p "${_tmp_source_path}" 1>/dev/null 2>&1 # save a copy of the source data to the scratch location - /bin/cp -pR "${_source_path}/" "${_tmp_source_path}" 1>/dev/null 2>&1 + /bin/cp -pR "${1}/" "${_tmp_source_path}" 1>/dev/null 2>&1 # destroy source location - /bin/rm -fr "${_source_path}" 1>/dev/null 2>&1 + /bin/rm -fr "${1}" 1>/dev/null 2>&1 } +# # Create a dataset with some special properties -_be_create_dataset() { +# +zfs_create_dataset() +{ local _source_path="${1}" local _target_dataset="${2}" - local _tmp_source_path="/tmp${_source_path}.old" # ensure we are starting off with a consistent state - _be_zfs_destroy "${_target_dataset}" + zfs_destroy "${_target_dataset}" # create the new target dataset - _be_zfs_create "${_source_path}" "${_target_dataset}" + zfs_create "${_source_path}" "${_target_dataset}" # set target dataset properties - _be_zfs_set_property "${_target_dataset}" setuid off - _be_zfs_set_property "${_target_dataset}" exec off - _be_zfs_set_property "${_target_dataset}" canmount noauto + zfs_set_prop "${_target_dataset}" setuid off + zfs_set_prop "${_target_dataset}" exec off + zfs_set_prop "${_target_dataset}" canmount noauto } +# # Force remount a dataset -_be_remount_ds() { - local _dataset="${1}" - - /sbin/zfs list -rH -o mountpoint,name,canmount,mounted -s mountpoint -t filesystem "${_dataset}" | \ +# +be_remount_ds() +{ + /sbin/zfs list -rH -o mountpoint,name,canmount,mounted -s mountpoint -t filesystem "${1}" | \ while read -r _mp _name _canmount _mounted ; do # skip filesystems that must not be mounted [ "$_canmount" = "off" ] && continue @@ -479,8 +527,11 @@ _be_remount_ds() { done } +# # Cleanup the migration -_be_post_migration() { +# +be_post_migration() +{ local _source_path="${1}" local _target_dataset="${2}" local _tmp_source_path="/tmp${_source_path}.old" @@ -492,55 +543,54 @@ _be_post_migration() { /bin/rm -fr "${_tmp_source_path}" 1>/dev/null 2>&1 # cycle the dataset mount - _be_remount_ds "${_target_dataset}" + be_remount_ds "${_target_dataset}" } +# # Do a complete migration given a source path and a target dataset -_be_do_migration() { +# +be_do_migration() { local _source_path="${1}" local _target_dataset="${2}" # short-circuit if the layout is already compatible and migration can be skipped - _be_is_path_on_root ${_source_path} && !([ -L "${_source_path}" ]) && return 0 + be_is_path_on_root ${_source_path} && !([ -L "${_source_path}" ]) && return 0 _echo -n "Migrating ${_source_path} to ZFS dataset ${_target_dataset}..." # move source data to a scratch location - _be_pre_migration "${_source_path}" "${_target_dataset}" + be_pre_migration "${_source_path}" "${_target_dataset}" # create the target dataset - _be_create_dataset "${_source_path}" "${_target_dataset}" + be_create_dataset "${_source_path}" "${_target_dataset}" # move source data from scratch location and cleanup - _be_post_migration "${_source_path}" "${_target_dataset}" + be_post_migration "${_source_path}" "${_target_dataset}" _echo " done." } -# Guard for _be_migrate -_be_migrate_guard() { - # various reasons to bail early from _be_migrate below... - _be_is_root_on_ufs -} - +# # Read a table of migrations and perform each one when on ZFS -_be_migrate() { +# +be_migrate() +{ # short-circuit if we are on UFS - _be_migrate_guard && return 0 + be_is_ufs_root && return 0 # the actual list of migrations to perform local _migration_table=" - # PATH DATASET - /cf $( _be_get_active_ds )/cf - /var/cache/pkg $( _be_get_active_ds )/var_cache_pkg - /var/db/pkg $( _be_get_active_ds )/var_db_pkg + # PATH DATASET + /cf $( be_active_ds )/cf + /var/cache/pkg $( be_active_ds )/var_cache_pkg + /var/db/pkg $( be_active_ds )/var_db_pkg " # END-QUOTE # now we do the migrations... echo "${_migration_table}" | while read -r _path _dataset; do # skip rows beginning with # (intended for comments as above) case "$_path" in "#"*|"") continue; esac - _be_do_migration "${_path}" "${_dataset}" + be_do_migration "${_path}" "${_dataset}" done return 0 } @@ -586,7 +636,7 @@ pkg_with_pb() { done fi - _exec "pkg-static ${_event_pipe} ${_cmd}" "${_msg}" + _exec "_pkg ${_event_pipe} ${_cmd}" "${_msg}" nc_pid="" } @@ -611,8 +661,7 @@ pkg_lock() { if [ "${_locked}" != "0" ]; then continue fi - _exec "pkg-static lock ${_pkg_name}" \ - "Locking package ${_pkg_name}" mute + _exec "_pkg lock ${_pkg_name}" "Locking package ${_pkg_name}" mute done done } @@ -630,41 +679,29 @@ pkg_unlock() { if [ "${_locked}" != "1" ]; then continue fi - _exec "pkg-static unlock ${_pkg_name}" \ - "Unlocking package ${_pkg_name}" mute + _exec "_pkg unlock ${_pkg_name}" "Unlocking package ${_pkg_name}" mute done done } set_vital_flag() { for _package in "$@"; do - local _vflag=$(pkg-static query %V ${_package} 2>/dev/null) - - [ "${_vflag}" != "0" ] \ - && continue - - _exec "pkg-static set -y -v 1 ${_package}" \ - "Setting vital flag on ${_package}" mute + local _vflag=$(_pkg query %V ${_package} 2>/dev/null) + [ "${_vflag}" != "0" ] && continue + _exec "_pkg set -y -v 1 ${_package}" "Setting vital flag on ${_package}" mute done } unset_vital_flag() { for _package in "$@"; do - local _vflag=$(pkg-static query %V ${_package} 2>/dev/null) - - [ "${_vflag}" = "0" ] \ - && continue - - _exec "pkg-static set -y -v 0 ${_package}" \ - "Removing vital flag from ${_package}" mute + local _vflag=$(_pkg query %V ${_package} 2>/dev/null) + [ "${_vflag}" = "0" ] && continue + _exec "_pkg set -y -v 0 ${_package}" "Removing vital flag from ${_package}" mute done } is_ce() { - if [ "${product_label}" == "pfSense" ]; then - return 0 - fi - return 1 + [ "${product_label}" == "pfSense" ] } is_plus() { @@ -868,6 +905,186 @@ pkg_set_origin() { done } +be_create() +{ + /sbin/bectl create "$@" 1>/dev/null 2>&1 +} + +be_activate() +{ + /sbin/bectl activate "$@" 1>/dev/null 2>&1 +} + +be_rename() +{ + /sbin/bectl rename "$@" 1>/dev/null 2>&1 +} + +be_is_mounted() +{ + be_get_mount "${1}" 1>/dev/null 2>&1 +} + +be_mount() +{ + if be_is_mounted "${1}"; then + return 0 + fi + + /sbin/bectl mount "$@" 1>/dev/null 2>&1 +} + +be_unmount() +{ + if be_is_mounted "${1}"; then + return 0 + fi + + /sbin/bectl unmount "$@" 1>/dev/null 2>&1 +} + +be_destroy() +{ + /sbin/bectl destroy "$@" 1>/dev/null 2>&1 +} + +be_reboot_deffered() +{ + [ "$( read_xml_tag.sh boolean system/firmware/bedeferreboot )" = "true" ] +} + +be_manual_verification() +{ + [ "$( read_xml_tag.sh boolean system/firmware/beverify )" = "true" ] +} + +be_manual_verification_timeout() +{ + local _timeout="$( read_xml_tag.sh number system/firmware/beverifytimeout )" + if [ "${_timeout}" = "NaN" ]; then + _timeout=300 + fi + + echo "${_timeout}" +} + +be_pkg_upgrade() +{ + local _mp + + UPGRADE_BE="$(be_active_name)" + + # Rename current boot environment + _echo -n ">>> Renaming current boot environment from ${UPGRADE_BE} to ${UPGRADE_BE}${BE_PREV_SUFFIX}..." + if ! be_rename "${UPGRADE_BE}" "${UPGRADE_BE}${BE_PREV_SUFFIX}"; then + _echo "failed." + _exit 1 + fi + _echo "done." + + # Create and mount the cloned boot environment + _echo -n ">>> Cloning current boot environment ${UPGRADE_BE}${BE_PREV_SUFFIX}..." + BE_UPGRADE_FAILED=1 + if ! be_create -r "${UPGRADE_BE}" || ! be_mount "${UPGRADE_BE}"; then + _echo "failed." + _exit 1 + fi + + # Set some ZFS user properties + be_set_prop "${UPGRADE_BE}" pfsense:upgrading yes + be_set_prop "${UPGRADE_BE}" pfsense:version "$(be_get_base_version "${UPGRADE_BE}")" + + # Toggle manual verification if requested + if be_manual_verification; then + be_set_prop "${UPGRADE_BE}" pfsense:verify yes + be_set_prop "${UPGRADE_BE}" pfsense:verifytimeout "$(be_manual_verification_timeout)" + fi + _echo "done." + + _mp="$(be_get_mount "${UPGRADE_BE}")" + + # Direct pkg into the BE root + PKG_OPTS="--chroot ${_mp}" + + unset_vital_flag "${cur_php_pkg}" + + # Do the package upgrade in the mounted environment + LAST_EXEC_CMD="pkg" + pkg_with_pb "upgrade" "Upgrading packages in cloned boot environment ${UPGRADE_BE}" + unset LAST_EXEC_CMD + + # Set some ZFS user properties (again) + be_unset_prop "${UPGRADE_BE}" pfsense:upgrading + be_set_prop "${UPGRADE_BE}" pfsense:version "$(be_get_base_version "${UPGRADE_BE}")" + + # Remove packages removed from remote repo + if is_pkg_installed ${pkg_prefix}\*; then + for _package in $(_pkg query %n $(_pkg info -E -g ${pkg_prefix}\*)); do + _pkg rquery -U -r ${product} %v ${_package} >/dev/null && continue + _exec "_pkg set -A 1 ${_package}" "Scheduling package ${_package} for removal" + done + fi + + # Cleanup caches and unnecessary packages before unmounting + _exec "_pkg autoremove" "Removing unnecessary packages" mute ignore_result + _exec "_pkg clean" "Cleanup pkg cache" mute ignore_result + + # All done working in the chroot + unset PKG_OPTS + + # Make sure that package installation scripts get executed during first boot + _echo -n ">>> Deferring package installation scripts..." + touch ${_mp}/deferred_pkg_install + _echo "done." + + LAST_EXEC_CMD="install-boot" + _exec "install-boot -d ${_mp} -y" "Upgrading boot code" + unset LAST_EXEC_CMD + + # Save a copy of latest finished upgrade process to the new BE + _echo -n ">>> Copying upgrade log..." + cp -f ${logfile} ${_mp}${upgrade_logfile} + _echo "done." + + # Check and backup RAM Disks + . /etc/rc.ramdisk_functions.sh + export CF_CONF_PATH="${_mp}/cf/conf" + ramdisk_make_backup + unset CF_CONF_PATH + + # The BE MUST be unmounted to ensure new data is properly committed to disk + _echo -n ">>> Unmounting upgraded boot environment..." + be_unmount -f "${UPGRADE_BE}" + if be_is_mounted "${UPGRADE_BE}"; then + _echo "failed." + _exit 1 + fi + _echo "done." + + # Temporarily activate upgraded boot environment + _echo -n ">>> Activating ${UPGRADE_BE} for the next boot only..." + be_activate -t "${UPGRADE_BE}" + _echo "done." + + # If we reach this point, we consider the upgrade successful + unset BE_UPGRADE_FAILED + + be_set_prop "${UPGRADE_BE}${BE_PREV_SUFFIX}" pfsense:lastbootonce "${UPGRADE_BE}" + + # Save a copy of latest finished upgrade process to the current BE too + cp -f ${logfile} ${upgrade_logfile} + + # Do the reboot if auto reboot is enabled + if be_reboot_deffered; then + _echo ">>> Deferring automatic reboot...done" + _exit 0 + fi + + do_reboot + + _exit 0 +} + pkg_upgrade() { # figure out which kernel variant is running export kernel_pkg=$(_pkg query %n $(_pkg info \ @@ -983,11 +1200,11 @@ pkg_upgrade() { # Cleanup BEs on incompatible systems. # Note: This is a no-op on UFS - _be_prune + be_prune # Migrate datasets on systems not compatible with BEs. # Note: This is a no-op on UFS - _be_migrate + be_migrate pkg_update force @@ -1104,13 +1321,16 @@ pkg_upgrade() { fi fi - # Prepare system by creating a rollback BE - # Note: This is a no-op on UFS (or on CE) - _be_prep_upgrade - if [ $? -ne 0 ]; then - _echo "ERROR: Unable to create automatic rollback boot environment." - _exit 1 + # + # Here we branch if we can use the new BE-based upgrade mechanism + # + if be_bootenv_check; then + be_pkg_upgrade + _exit 0 fi + # + # Otherwise, we continue on through the old update code path + # # Detect the move from suricata to suricata4 if is_pkg_installed ${product}-pkg-suricata \ @@ -1139,7 +1359,7 @@ pkg_upgrade() { # Unlock pkg to make sure it's downloaded if [ -n "${NEW_MAJOR}" ]; then pkg_unlock pkg - unlock_pkg="" + unset unlock_pkg fi # Download all upgrade packages first @@ -1230,21 +1450,16 @@ pkg_upgrade() { fi fi + # Remove packages removed from remote repo if is_pkg_installed ${pkg_prefix}\*; then - for _package in $(_pkg query %n $(_pkg info -E -g \ - ${pkg_prefix}\*)); do - _pkg rquery -U -r ${product} %v \ - ${_package} >/dev/null && continue - - # This package was removed from remote repo - # Make sure it's uninstalled - _exec "pkg-static set -A 1 ${_package}" \ - "Scheduling package ${_package} for removal" + for _package in $(_pkg query %n $(_pkg info -E -g ${pkg_prefix}\*)); do + _pkg rquery -U -r ${product} %v ${_package} >/dev/null && continue + _exec "_pkg set -A 1 ${_package}" "Scheduling package ${_package} for removal" done fi - _exec "pkg-static autoremove" "Removing unnecessary packages" \ - mute ignore_result + # Cleanup caches and unnecessary packages before unmounting + _exec "_pkg autoremove" "Removing unnecessary packages" mute ignore_result if [ -n "${NEW_MAJOR}" ]; then _pkg annotate -q -M ${kernel_pkg} new_major 1 @@ -1254,9 +1469,13 @@ pkg_upgrade() { # sqlite database path in pkg. # pkg(8) needs to be updated while the network is up to # update/recreate the local database. + # In this specific case - from 23.09 and 23.09.1 - it + # is safe to update the pkg before booting the new kernel. # See #15964. # - if [ "$(compare_pkg_version pkg)" = "<" ]; then + if [ "$(_pkg query %v "${product}" | \ + /usr/bin/grep -c "23.09")" = "1" ] && \ + [ "$(compare_pkg_version pkg)" = "<" ]; then pkg_unlock pkg _exec "pkg-static upgrade${dont_update} pkg" \ "Upgrading pkg" mute @@ -1363,6 +1582,11 @@ pkg_upgrade() { find / -name \*.pkgsave -delete fi + # Upgrade the boot code + LAST_EXEC_CMD="install-boot" + _exec "install-boot -y" "Upgrading boot code" + unset LAST_EXEC_CMD + _pkg annotate -q -M ${kernel_pkg} next_stage 3 next_stage=3 @@ -1391,6 +1615,11 @@ pkg_upgrade() { _exec "/etc/rc.d/ldconfig onestart" \ "Updating ldconfig" mute ignore_result /etc/rc.php_ini_setup >/dev/null 2>&1 + + # Upgrade the boot code (this might be redundant from stage 2, oh well...) + LAST_EXEC_CMD="install-boot" + _exec "install-boot -y" "Upgrading boot code" + unset LAST_EXEC_CMD fi # Cleanup a possible crash report created during PHP upgrade @@ -1404,9 +1633,8 @@ pkg_upgrade() { _pkg annotate -q -D ${kernel_pkg} next_stage # cleanup caches - _exec "pkg-static autoremove" "Removing unnecessary packages" \ - mute ignore_result - _exec "pkg-static clean" "Cleanup pkg cache" mute ignore_result + _exec "_pkg autoremove" "Removing unnecessary packages" mute ignore_result + _exec "_pkg clean" "Cleanup pkg cache" mute ignore_result fi gitsync=$(read_xml_tag.sh boolean system/gitsync/synconupgrade) @@ -1476,6 +1704,14 @@ check_upgrade() { local _core_pkgs=$(pkg-static query -e \ "%n ~ ${product}-kernel-* || %n ~ ${product}-base*" %n 2>/dev/null) + # Do not upgrade the Netgate SG-1000 + if [ "$(/bin/kenv -q uboot.board_name 2> /dev/null)" = "A335uFW" ]; then + MESSAGE="ERROR: The Netgate SG-1000 cannot be upgraded. \ +There are no new releases for the SG-1000." + notice "${MESSAGE}" + _echo "${MESSAGE}" + exit 1 + fi # Do not upgrade while wg interfaces are assigned if sed '//,/<\/interfaces>/!d' /cf/conf/config.xml \ | grep -q 'wg'; then @@ -1490,6 +1726,47 @@ check_upgrade() { "upgrading." fi + # Do not upgrade arm64 if efi is too small + local _model=$(/bin/kenv -q smbios.system.product 2>/dev/null) + if [ "${_model}" = "mvebu_armada-37xx" ] || [ "${_model}" = "SG-2100" ] + then + local efidev sed_efidev efityp efisz + + # Restore EFISYS FAT label on ESP + label_esp + + efidev=$(geom label status -s | grep msdosfs/EFISYS | awk '{print $3}') + efidev=${efidev:-'NOTFOUND'} + sed_efidev=$(echo ${efidev} | sed 's/\//\\\//g') #escaped slashes for sed patterns + if [ "$efidev" != 'NOTFOUND' ]; then + efityp=$(geom -p "${efidev}" | sed -E "/Name: ${sed_efidev}/,/[[:blank:]]+type:/ !d" | tail -n 1 | awk '{print $2}') + fi + efityp=${efityp:-'NOTFOUND'} + + if be_is_zfs_root && ([ "${efigeom}" == 'NOTFOUND' ] || [ "${efidev}" == 'NOTFOUND' ] || [ "$efityp" != 'efi' ]); then + MESSAGE=$(printf "%s" \ + "ERROR: Cannot update the EFI loader on this device. " \ + "Contact TAC at " \ + "https://www.netgate.com/tac-support-request for " \ + "assistance upgrading this device.") + notice "${MESSAGE}" + _echo "${MESSAGE}" + _exit 1 + fi + + efisz=$(geom -p "${efidev}" 2> /dev/null | sed -E "/Name: ${sed_efidev}/,/[[:blank:]]+length:/ !d" | tail -n 1 | awk '{print $2}') + if ([ -z "${efisz}" ] && be_is_zfs_root) || ([ -n "${efisz}" ] && [ "${efisz}" -lt "1048576" ]); then + MESSAGE=$(printf "%s" \ + "ERROR: The EFI partition on this device is too small " \ + "to receive the updated arm64 EFI loader. Contact TAC " \ + "at https://www.netgate.com/tac-support-request for " \ + "assistance upgrading this device.") + notice "${MESSAGE}" + _echo "${MESSAGE}" + _exit 1 + fi + fi + [ -z "${_skip_update}" ] \ && pkg_update "" mute @@ -1640,10 +1917,7 @@ check_upgrade() { } is_pkg_installed() { - local _pkg_name="${1}" - - pkg-static info -e ${_pkg_name} 2>/dev/null - return $? + _pkg info -e "${1}" 2>/dev/null } compare_pkg_version() { @@ -1794,14 +2068,6 @@ do_reboot() { _msg="in ${reboot_after} seconds." fi - # Ensure that we are *always* rebooting into the BE that is actively being upgraded - # Note: This is a no-op on UFS - _be_prep_reboot - if [ $? -ne 0 ]; then - _echo "ERROR: Unable to activate the boot environment being upgraded ($( _be_get_active ))." - _exit 1 - fi - local _complete="System is going to be upgraded" if [ -z "${dont_reboot}" ]; then _echo "${_complete}. Rebooting ${_msg}" @@ -1822,6 +2088,42 @@ do_reboot() { fi } +label_esp() { + local efipart + local _efipart + local tmpdir + + # efi label is found, drop out + efipart=$(geom label status -s | grep 'msdosfs/EFISYS' | awk '{print $3}') + if [ -n "${efipart}" ]; then + return + fi + + # otherwise, update first efi filesystem on a disk that has a freebsd partition + if [ -z "${efipart}" ]; then + for _disk in $(geom part show | awk '/^=>/ && $5 ~ "GPT|MBR" {print $4}'); do + echo "Looking for EFI partition on disk ${_disk}" + _efipart=$(gpart show -p ${_disk} | awk '$4 == "efi" {part=$3} $4 ~ /freebsd/ {print part}') + if [ -n "${_efipart}" ] && [ "$(fstyp /dev/${_efipart})" == "msdosfs" ]; then + efipart="${_efipart}" + break + fi + done + fi + if [ -n "${efipart}" ]; then + echo "Restoring EFISYS FAT label to ${efipart}" + tmpdir=$(mktemp -d) && trap "rm -rf ${tmpdir}" EXIT + dd if=/dev/${efipart} count=1 of=${tmpdir}/boot.bin 2>/dev/null && + dd if=${tmpdir}/boot.bin of=${tmpdir}/pre.bin bs=1 count=0x2b 2>/dev/null && + dd if=${tmpdir}/boot.bin of=${tmpdir}/suf.bin bs=1 skip=0x36 2>/dev/null && + printf "%-11s" "EFISYS" >> ${tmpdir}/pre.bin && + cat ${tmpdir}/pre.bin ${tmpdir}/suf.bin > ${tmpdir}/boot.bin && + dd if=${tmpdir}/boot.bin of=/dev/${efipart} 2>/dev/null + rm -rf ${tmpdir} + trap - EXIT + fi +} + export LANG=C pid_file="/var/run/$(basename $0).pid" @@ -2081,7 +2383,7 @@ case "$?" in 14) # New repo may contain newer pkg if [ "$(compare_pkg_version pkg)" = "<" ]; then - _exec "pkg-static upgrade pkg" "Upgrading pkg" mute + _exec "_pkg upgrade pkg" "Upgrading pkg" mute fi ;; esac @@ -2118,8 +2420,10 @@ fi case "${action}" in check | checkall) + LAST_EXEC_CMD="check_upgrade" check_upgrade _exit $? + unset LAST_EXEC_CMD ;; upgrade) pkg_upgrade