1#!/bin/sh 2# 3# Copyright (c) 2016 Will Andrews 4# All rights reserved. 5# 6# Redistribution and use in source and binary forms, with or without 7# modification, are permitted provided that the following conditions 8# are met: 9# 1. Redistributions of source code must retain the above copyright 10# notice, this list of conditions and the following disclaimer 11# in this position and unchanged. 12# 2. Redistributions in binary form must reproduce the above copyright 13# notice, this list of conditions and the following disclaimer in the 14# documentation and/or other materials provided with the distribution. 15# 16# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 17# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 18# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 19# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 20# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 21# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 23# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 25# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26# 27# 28## 29# Install a boot environment using the current FreeBSD source tree. 30# Requires a fully built world & kernel. 31# 32# Non-base tools required: pkg 33# 34# In a sandbox for the new boot environment, this script also runs etcupdate 35# and pkg upgrade automatically in the sandbox. Upon successful completion, 36# the system will be ready to boot into the new boot environment. Upon 37# failure, the target boot environment will be destroyed. In all cases, the 38# running system is left untouched. 39# 40## Usage: 41# beinstall [optional world/kernel flags e.g. KERNCONF] 42# 43## User modifiable variables - set these in the environment if desired. 44# Utility to manage ZFS boot environments. 45BE_UTILITY="${BE_UTILITY:-"bectl"}" 46# If not empty, 'pkg upgrade' will be skipped. 47NO_PKG_UPGRADE="${NO_PKG_UPGRADE:-""}" 48# Config updater - 'etcupdate' is supported. Set to an empty string to skip. 49CONFIG_UPDATER="${CONFIG_UPDATER:-"etcupdate"}" 50# Flags for etcupdate if used. 51ETCUPDATE_FLAGS="${ETCUPDATE_FLAGS:-"-BF"}" 52 53 54######################################################################## 55## Functions 56cleanup() { 57 [ -z "${cleanup_commands}" ] && return 58 echo "Cleaning up ..." 59 for command in ${cleanup_commands}; do 60 ${command} 61 done 62} 63 64errx() { 65 cleanup 66 echo "error: $@" 67 exit 1 68} 69 70rmdir_be() { 71 chflags -R noschg ${BE_MNTPT} 72 rm -rf ${BE_MNTPT} 73} 74 75unmount_be() { 76 mount | grep " on ${BE_MNTPT}" | awk '{print $3}' | sort -r | xargs -t umount -f 77} 78 79copy_pkgs() { 80 # Before cleaning up, try to save progress in pkg(8) updates, to 81 # speed up future updates. This is only called on the error path; 82 # no need to run on success. 83 echo "Rsyncing back newly saved packages..." 84 rsync -av --progress ${BE_MNTPT}/var/cache/pkg/. /var/cache/pkg/. 85} 86 87cleanup_be() { 88 # Before destroying, unmount any child filesystems that may have 89 # been mounted under the boot environment. Sort them in reverse 90 # order so children are unmounted first. 91 unmount_be 92 # Finally, clean up any directories that were created by the 93 # operation, via cleanup_be_dirs(). 94 if [ -n "${created_be_dirs}" ]; then 95 chroot ${BE_MNTPT} /bin/rm -rf ${created_be_dirs} 96 fi 97 ${BE_UTILITY} destroy -F ${BENAME} 98} 99 100create_be_dirs() { 101 echo "${BE_MNTPT}: Inspecting dirs $*" 102 for dir in $*; do 103 curdir="$dir" 104 topdir="$dir" 105 while :; do 106 [ -e "${BE_MNTPT}${curdir}" ] && break 107 topdir=$curdir 108 curdir=$(dirname ${curdir}) 109 done 110 [ "$curdir" = "$dir" ] && continue 111 112 # Add the top-most nonexistent directory to the list, then 113 # mkdir -p the innermost directory specified by the argument. 114 # This way the least number of directories are rm'd directly. 115 created_be_dirs="${topdir} ${created_be_dirs}" 116 echo "${BE_MNTPT}: Created ${dir}" 117 mkdir -p ${BE_MNTPT}${dir} || return $? 118 done 119 return 0 120} 121 122update_etcupdate_pre() { 123 ${ETCUPDATE_CMD} -p -s ${srcdir} -D ${BE_MNTPT} ${ETCUPDATE_FLAGS} || return $? 124 ${ETCUPDATE_CMD} resolve -D ${BE_MNTPT} || return $? 125} 126 127update_etcupdate() { 128 chroot ${BE_MNTPT} \ 129 ${ETCUPDATE_CMD} -s ${srcdir} ${ETCUPDATE_FLAGS} || return $? 130 chroot ${BE_MNTPT} ${ETCUPDATE_CMD} resolve 131} 132 133 134# Special command-line subcommand that can be used to do a full cleanup 135# after a manual post-mortem has been completed. 136postmortem() { 137 [ -n "${BENAME}" ] || errx "Must specify BENAME" 138 [ -n "${BE_MNTPT}" ] || errx "Must specify BE_MNTPT" 139 echo "Performing post-mortem on BE ${BENAME} at ${BE_MNTPT} ..." 140 unmount_be 141 rmdir_be 142 echo "Post-mortem cleanup complete." 143 echo "To destroy the BE (recommended), run: ${BE_UTILITY} destroy ${BENAME}" 144 echo "To instead continue with the BE, run: ${BE_UTILITY} activate ${BENAME}" 145} 146 147if [ -n "$BEINSTALL_CMD" ]; then 148 ${BEINSTALL_CMD} $* 149 exit $? 150fi 151 152if [ "$(basename -- "${BE_UTILITY}")" = "bectl" ]; then 153 ${BE_UTILITY} check || errx "${BE_UTILITY} sanity check failed" 154fi 155 156cleanup_commands="" 157trap 'errx "Interrupt caught"' HUP INT TERM 158 159[ "$(whoami)" != "root" ] && errx "Must be run as root" 160 161[ ! -f "Makefile.inc1" ] && errx "Must be in FreeBSD source tree" 162srcdir=$(pwd) 163objdir=$(make -V .OBJDIR 2>/dev/null) 164[ ! -d "${objdir}" ] && errx "Must have built FreeBSD from source tree" 165 166## Constants 167ETCUPDATE_CMD="${srcdir}/usr.sbin/etcupdate/etcupdate.sh" 168 169# May be a worktree, in which case .git is a file, not a directory. 170if [ -e .git ] ; then 171 commit_time=$(git show -s --format='%ct' 2>/dev/null) 172 [ $? -ne 0 ] && errx "Can't lookup git commit timestamp" 173 commit_ts=$(date -r ${commit_time} '+%Y%m%d.%H%M%S') 174elif [ -d .svn ] ; then 175 if [ -e /usr/bin/svnlite ]; then 176 svn=/usr/bin/svnlite 177 elif [ -e /usr/local/bin/svn ]; then 178 svn=/usr/local/bin/svn 179 else 180 errx "Unable to find subversion" 181 fi 182 commit_ts="$( "$svn" info --show-item last-changed-date | sed -e 's/\..*//' -e 's/T/./' -e 's/-//g' -e s'/://g' )" 183 [ $? -ne 0 ] && errx "Can't lookup Subversion commit timestamp" 184else 185 errx "Unable to determine source control type" 186fi 187 188commit_ver=$(${objdir}/bin/freebsd-version/freebsd-version -u 2>/dev/null) 189[ -z "${commit_ver}" ] && errx "Unable to determine FreeBSD version" 190 191BENAME="${commit_ver}-${commit_ts}" 192 193BE_TMP=$(mktemp -d /tmp/beinstall.XXXXXX) 194[ $? -ne 0 -o ! -d ${BE_TMP} ] && errx "Unable to create mountpoint" 195[ -z "$NO_CLEANUP_BE" ] && cleanup_commands="rmdir_be ${cleanup_commands}" 196BE_MNTPT=${BE_TMP}/mnt 197mkdir -p ${BE_MNTPT} 198 199${BE_UTILITY} create ${BENAME} >/dev/null || errx "Unable to create BE ${BENAME}" 200[ -z "$NO_CLEANUP_BE" ] && cleanup_commands="cleanup_be ${cleanup_commands}" 201 202${BE_UTILITY} mount ${BENAME} ${BE_TMP}/mnt || errx "Unable to mount BE ${BENAME}." 203 204echo "Mounted ${BENAME} to ${BE_MNTPT}, performing install/update ..." 205make "$@" DESTDIR=${BE_MNTPT} installkernel || errx "Installkernel failed!" 206if [ -n "${CONFIG_UPDATER}" ]; then 207 "update_${CONFIG_UPDATER}_pre" 208 [ $? -ne 0 ] && errx "${CONFIG_UPDATER} (pre-world) failed!" 209fi 210 211# Mount the source and object tree within the BE in order to account for any 212# changes applied by the pre-installworld updater. Cleanup any directories 213# created if they didn't exist previously. 214create_be_dirs "${srcdir}" "${objdir}" || errx "Unable to create BE dirs" 215mount -t nullfs "${srcdir}" "${BE_MNTPT}${srcdir}" || errx "Unable to mount src" 216mount -t nullfs "${objdir}" "${BE_MNTPT}${objdir}" || errx "Unable to mount obj" 217mount -t devfs devfs "${BE_MNTPT}/dev" || errx "Unable to mount devfs" 218 219chroot ${BE_MNTPT} make "$@" -C ${srcdir} installworld || \ 220 errx "Installworld failed!" 221 222if [ -n "${CONFIG_UPDATER}" ]; then 223 "update_${CONFIG_UPDATER}" 224 [ $? -ne 0 ] && errx "${CONFIG_UPDATER} (post-world) failed!" 225fi 226 227if which rsync >/dev/null 2>&1; then 228 cleanup_commands="copy_pkgs ${cleanup_commands}" 229fi 230 231BE_PKG="chroot ${BE_MNTPT} env ASSUME_ALWAYS_YES=true pkg" 232if [ -z "${NO_PKG_UPGRADE}" ]; then 233 ${BE_PKG} update || errx "Unable to update pkg" 234 ${BE_PKG} upgrade || errx "Unable to upgrade pkgs" 235fi 236 237if [ -n "$NO_CLEANUP_BE" ]; then 238 echo "Boot Environment ${BENAME} may be examined in ${BE_MNTPT}." 239 echo "Afterwards, run this to cleanup:" 240 echo " env BENAME=${BENAME} BE_MNTPT=${BE_MNTPT} BEINSTALL_CMD=postmortem $0" 241 exit 0 242fi 243 244unmount_be || errx "Unable to unmount BE" 245rmdir_be || errx "Unable to cleanup BE" 246${BE_UTILITY} activate ${BENAME} || errx "Unable to activate BE" 247echo 248${BE_UTILITY} list 249echo 250echo "Boot environment ${BENAME} setup complete; reboot to use it." 251