#!/bin/sh -e # Copyright (c) 2012-2022 Slawomir Wojciech Wojtczak (vermaden) # Copyright (c) 2012-2013 Bryan Drewery (bdrewery) # Copyright (c) 2012-2013 Mike Clarke (rawthey) # Copyright (c) 2013 Dancho Penev (dpslavov) # Copyright (c) 2020 J.M. Rivera (JRGTH) # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that following conditions are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS 'AS IS' AND ANY # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. unset LC_ALL unset LANG PATH=${PATH}:/bin:/usr/bin:/usr/local/bin:/sbin:/usr/sbin:/usr/local/sbin # check FreeBSD version if [ $( uname -r | cut -d '-' -f1 | cut -d '.' -f1 ) -lt 8 ] then echo "ERROR: beadm(8) works on FreeBSD 8.0 or later" exit 1 fi # display version if [ "${1}" = "--version" -o \ "${1}" = "-version" -o \ "${1}" = "version" ] then echo " " echo " ___ ___ __ ____ __ " echo " / / / / / // \\\ \ " echo " / / ____ ____ ___/ /___ ___ / // / / \ \ " echo " / \ / _ \ / \ / // \ \/ / \ \ / / " echo " / / // ___// / // / // / / /\ \ / / // / " echo " \____/ \____/ \_____\\\____//__/__/__/ \_\\\____//_/ " echo echo "beadm 1.3.6 2022/06/01" echo exit 0 fi # display usage information __usage() { local NAME=${0##*/} echo "usage:" echo " ${NAME} activate " echo " ${NAME} create [-e nonActiveBe | -e beName@snapshot] " echo " ${NAME} create " echo " ${NAME} destroy [-F] " echo " ${NAME} export [-v] " echo " ${NAME} import [-v] " echo " ${NAME} list [-a] [-s] [-D] [-H]" echo " ${NAME} rename " echo " ${NAME} mount [mountpoint]" echo " ${NAME} { umount | unmount } [-f] " echo " ${NAME} chroot " echo " ${NAME} reroot " echo " ${NAME} version" exit 1 } # check if /boot is symlink to bootpool on this system BOOTPATH= if [ -L '/boot' ] && [ -d '/boot' ] then BOOTPOOL=$( readlink '/boot' | awk -F '/' '{print $1}' ) BOOTPROP=$( zpool list -H -o bootfs ${BOOTPOOL} ) if [ "${BOOTPROP}" = "-" ] then BOOTPATH=/${BOOTPOOL} fi fi # check if system has /boot/grub/grub.cfg file and update it __update_grub() { if [ -e /boot/grub/grub.cfg ] then if grub-mkconfig -o /boot/grub/grub.cfg 1> /dev/null 2> /dev/null then echo "GRUB configuration updated successfully" else echo "WARNING: Failed to update GRUB configuration" fi fi } # check if boot environment exists __be_exist() { # 1=DATASET if ! zfs list -H -o name ${1} 1> /dev/null 2> /dev/null then echo "ERROR: Boot environment '${1##*/}' does not exist" exit 1 fi } # check if argument is snapshot __be_snapshot() { # 1=DATASET/SNAPSHOT echo "${1}" | grep -q "@" 2> /dev/null } # check if boot environment is mounted __be_mounted() { # 1=BE mount 2> /dev/null | grep -q -E "^${1} " 2> /dev/null } # check if boot environment is clone __be_clone() { # 1=DATASET if zfs list ${1} 1> /dev/null 2> /dev/null then local ORIGIN="$( zfs list -H -o origin ${1} )" if [ "${ORIGIN}" = "-" ] then # boot environment is not a clone return 1 else # boot environment is a clone return 0 fi else # boot environment does not exist return 2 fi } # create new boot environment __be_new() { # 1=SOURCE 2=TARGET local NAME_NEW=$( basename "${2}" ) local NAME_SANITY=$( echo "${NAME_NEW}" | tr -c -d 'a-zA-Z0-9-_.,' ) if [ "${NAME_NEW}" != "${NAME_SANITY}" ] then echo "ERROR: Can not create boot environment with '${NAME_NEW}' name" echo echo "Allowed characters for boot environment are:" echo " alphanumeric characters: 'a-z' 'A-Z' '0-9'" echo " hypen: '-'" echo " underscore: '_'" echo " dot: '.'" echo " comma: ','" echo echo "Name '${NAME_SANITY}' which uses only allowed characters can be used" echo exit 1 fi unset NAME_NEW unset NAME_SANITY local SOURCE=$( echo ${1} | cut -d '@' -f 1 ) local ENTROPY=0 # secure current /boot/entropy file if [ -f /boot/entropy ] then if ! cp -p /boot/entropy /boot/entropy.OLD 1> /dev/null 2> /dev/null then echo "ERROR: Can not copy current '/boot/entropy' file" exit 1 fi ENTROPY=1 fi # create new /boot/entropy file that would be used in new boot environment if ! dd if=/dev/random of=/boot/entropy bs=4096 count=1 1> /dev/null 2> /dev/null then echo "ERROR: Can not generate new '/boot/entropy' file" exit 1 fi # create snapshot that will be used as base for new boot environment if __be_snapshot ${1} then # create boot environment from snapshot local SNAPSHOT=$( echo ${1} | cut -d '@' -f 2 ) zfs list -r -H -t filesystem -o name ${SOURCE} \ | while read FS do if ! zfs list -H -o name ${FS}@${SNAPSHOT} 1> /dev/null 2> /dev/null then echo "ERROR: Child snapshot '${FS}@${SNAPSHOT}' does not exist" if [ ${ENTROPY} -ne 0 ] then if ! mv /boot/entropy.OLD /boot/entropy 1> /dev/null 2> /dev/null then echo "WARNING: Can not bring back original '/boot/entropy' file" fi fi exit 1 fi done else # create boot environment from other boot environment if zfs list -H -o name ${1}@${2##*/} 1> /dev/null 2> /dev/null then echo "ERROR: Snapshot '${1}@${2##*/}' already exists" if [ ${ENTROPY} -ne 0 ] then if ! mv /boot/entropy.OLD /boot/entropy 1> /dev/null 2> /dev/null then echo "WARNING: Can not bring back original '/boot/entropy' file" fi fi exit 1 fi # snapshot format FMT=$( date "+%Y-%m-%d-%H:%M:%S" ) if ! zfs snapshot -r ${1}@${FMT} 1> /dev/null 2> /dev/null then echo "ERROR: Cannot create snapshot '${1}@${FMT}'" if [ ${ENTROPY} -ne 0 ] then if ! mv /boot/entropy.OLD /boot/entropy 1> /dev/null 2> /dev/null then echo "WARNING: Can not bring back original '/boot/entropy' file" fi fi exit 1 fi fi # bring back secured /boot/entropy.OLD file if [ -f /boot/entropy.OLD ] then if ! mv /boot/entropy.OLD /boot/entropy 1> /dev/null 2> /dev/null then echo "WARNING: Can not bring back original '/boot/entropy' file" fi fi unset ENTROPY # clone properties of source boot environment zfs list -H -o name -r ${SOURCE} \ | grep -v '@' \ | while read FS do local OPTS="" while read NAME PROPERTY VALUE do case ${PROPERTY} in (snapshot_count|filesystem_count) # read only property - skip it ;; (*) if [ "${VALUE}" != "" ] then local OPTS="-o ${PROPERTY}=\"${VALUE}\" ${OPTS}" else local OPTS="" break fi ;; esac done << EOF $( zfs get -o name,property,value -s local,received -H all ${FS} | awk '!/[\t ]canmount[\t ]/' ) EOF DATASET=$( echo ${FS} | awk '{print $1}' | sed -E s@"^${POOL}\/${BEDS}\/${SOURCE##*/}"@"${POOL}\/${BEDS}\/${2##*/}"@g ) if __be_snapshot ${1} then eval "zfs clone -o canmount=off ${OPTS} ${FS}@${1##*@} ${DATASET}" else eval "zfs clone -o canmount=off ${OPTS} ${FS}@${FMT} ${DATASET}" fi done # check if we need to update grub if [ "${GRUB}" = YES ] then __update_grub fi } ROOTFS=$( mount | awk '/ \/ / {print $1}' ) if echo ${ROOTFS} | grep -q -m 1 -E "^/dev/" then echo "ERROR: This system does not boot from ZFS pool" exit 1 fi POOL=$( echo ${ROOTFS} | awk -F '/' '{print $1}' ) if [ $( echo ${ROOTFS} | awk -F '/' '{print NF}' ) -lt 3 ] then echo "ERROR: This system is not configured for boot environments" exit 1 fi BOOTFS=$( zpool list -H -o bootfs ${POOL} ) if [ -z "${BOOTFS}" -o "${BOOTFS}" = "-" ] then echo "ERROR: ZFS boot pool '${POOL}' has unset 'bootfs' property" exit 1 fi # load settings from config file if [ -f /usr/local/etc/beadm.conf ] then . /usr/local/etc/beadm.conf fi # update GRUB bootloader instead of FreeBSD loader(8) : ${GRUB="NO"} # use other prefix then the 'pool/ROOT/bename' default : ${BEDS="$( echo ${ROOTFS} | awk -F '/' '{print $2}' )"} # main option selection case ${1} in (list) # -------------------------------------------------------------------- OPTION_a=0 OPTION_D=0 OPTION_s=0 shift while getopts "aDHs" OPT do case ${OPT} in (a) OPTION_a=1 ;; (D) OPTION_D=1 ;; (H) OPTION_H=1 ;; (s) OPTION_s=1 OPTION_a=1 ;; (*) __usage ;; esac done awk -v POOL="${POOL}" \ -v BEDS="${BEDS}" \ -v ROOTFS="${ROOTFS}" \ -v BOOTFS="${BOOTFS}" \ -v OPTION_a="${OPTION_a}" \ -v OPTION_D="${OPTION_D}" \ -v OPTION_H="${OPTION_H}" \ -v OPTION_s="${OPTION_s}" \ 'function __normalize(VALUE) { if(VALUE == "-" || VALUE == 0) return 0 else return substr(VALUE, 1, length(VALUE) - 1) * MULTIPLIER[substr(VALUE, length(VALUE))] } function __show_units(VALUE) { if(VALUE < 1024) { UNIT = "K"; } else if(VALUE < 1048576) { VALUE /= 1024; UNIT = "M"; } else if(VALUE < 1073741824) { VALUE /= 1048576; UNIT = "G"; } else if(VALUE < 1099511627776) { VALUE /= 1073741824; UNIT = "T"; } else if(VALUE < 1125899906842624) { VALUE /= 1099511627776; UNIT = "P"; } else if(VALUE < 1152921504606846976) { VALUE /= 1125899906842624; UNIT = "E"; } else { VALUE /= 1152921504606846976; UNIT = "Z"; } return sprintf("%.1f%s", VALUE, UNIT) } function __get_bename(BENAME) { sub(BENAME_BEGINS_WITH "\/", "", BENAME) sub("/.*", "", BENAME) return BENAME } function __convert_date(DATE) { CMD_DATE = "date -j -f \"%a %b %d %H:%M %Y\" \"" DATE "\" +\"%Y-%m-%d %H:%M\"" CMD_DATE | getline NEW close(CMD_DATE) return NEW } BEGIN { BENAME_BEGINS_WITH = POOL "/" BEDS MULTIPLIER["K"] = 1 MULTIPLIER["M"] = 1024 MULTIPLIER["G"] = 1048576 MULTIPLIER["T"] = 1073741824 MULTIPLIER["P"] = 1099511627776 MULTIPLIER["E"] = 1125899906842624 MULTIPLIER["Z"] = 1152921504606846976 MOUNTPOINT_LENGTH = 10 FSNAME_LENGTH = 2 if(OPTION_a == 1) FSNAME_LENGTH = 19 CMD_MOUNT="mount" while(CMD_MOUNT | getline) if($1 ~ "^" BENAME_BEGINS_WITH) MOUNTS[$1] = $3 close(CMD_MOUNT) FS = "\\t" CMD_ZFS_LIST = "zfs list -H -t filesystem,snapshot,volume -s creation -o name,used,usedds,usedbysnapshots,usedrefreserv,refer,creation,origin -r " while(CMD_ZFS_LIST BENAME_BEGINS_WITH | getline) { if($1 != BENAME_BEGINS_WITH) { FSNAME = $1 FSNAMES[length(FSNAMES) + 1] = FSNAME USED = __normalize($2) USEDBYDATASET = __normalize($3) USEDBYSNAPSHOTS = __normalize($4) USEDREFRESERV = __normalize($5) REFER[FSNAME] = __normalize($6) CREATIONS[FSNAME] = $7 ORIGINS[FSNAME] = $8 if(FSNAME ~ /@/) SPACES[FSNAME] = USED else { SPACES[FSNAME] = USEDBYDATASET + USEDREFRESERV if(OPTION_D != 1) SPACES[FSNAME] += USEDBYSNAPSHOTS BE = " " __get_bename(FSNAME) " " if(index(BELIST, BE) == 0) BELIST = BELIST " " BE MOUNTPOINT = MOUNTS[FSNAME] if(MOUNTPOINT) { if((OPTION_a == 0 && FSNAME == (BENAME_BEGINS_WITH "/" __get_bename(FSNAME))) || (OPTION_a == 1)) { LM = length(MOUNTPOINT) if(LM > MOUNTPOINT_LENGTH) MOUNTPOINT_LENGTH = LM } } else MOUNTPOINT = "-" } if(OPTION_a == 1) LF = length(FSNAME) else if(FSNAME !~ /@/) LF = length(__get_bename(FSNAME)) if(LF > FSNAME_LENGTH) FSNAME_LENGTH = LF } } close(CMD_ZFS_LIST BENAME_BEGINS_WITH) split(BELIST, BENAMES, " ") if(OPTION_a == 1) { BE_HEAD = "BE/Dataset/Snapshot" printf "%-" FSNAME_LENGTH + 2 "s %-6s %-" MOUNTPOINT_LENGTH "s %6s %s\n", BE_HEAD, "Active", "Mountpoint", "Space", "Created" } else if(OPTION_H == 1) BE_HEAD = "" else { BE_HEAD = "BE" printf "%-" FSNAME_LENGTH "s %-6s %-" MOUNTPOINT_LENGTH "s %6s %s\n", BE_HEAD, "Active", "Mountpoint", "Space", "Created" } if(OPTION_s != 1) SNAPSHOT_FILTER = "(/[^@]*)?$" for(I = 1; I <= length(BENAMES); I++) { BENAME = BENAMES[I] if(OPTION_a == 1) { printf "\n" print BENAME for(J = 1; J <= length(FSNAMES); J++) { FSNAME = FSNAMES[J] if(FSNAME ~ "^" BENAME_BEGINS_WITH "/" BENAME SNAPSHOT_FILTER) { ACTIVE = "" if(FSNAME == ROOTFS) ACTIVE = ACTIVE "N" if(FSNAME == BOOTFS) ACTIVE = ACTIVE "R" if(! ACTIVE) ACTIVE = "-" MOUNTPOINT = MOUNTS[FSNAME] if(! MOUNTPOINT) MOUNTPOINT = "-" printf " %-" FSNAME_LENGTH "s %-6s %-" MOUNTPOINT_LENGTH "s %6s %s\n", FSNAME, ACTIVE, MOUNTPOINT, __show_units(SPACES[FSNAME]), __convert_date(CREATIONS[FSNAME]) ORIGIN = ORIGINS[FSNAME] ORIGIN_DISPLAY = ORIGIN sub(BENAME_BEGINS_WITH "/", "", ORIGIN_DISPLAY) if(ORIGIN != "-") { if(OPTION_D == 1) SPACE = REFER[ORIGIN] else SPACE = SPACES[ORIGIN] printf " %-" FSNAME_LENGTH "s %-6s %-" MOUNTPOINT_LENGTH "s %6s %s\n", " " ORIGIN_DISPLAY, "-", "-", __show_units(SPACE), __convert_date(CREATIONS[ORIGIN]) } } } } else { SPACE = 0 ACTIVE = "" NAME = BENAME_BEGINS_WITH "/" BENAME if(NAME == ROOTFS) ACTIVE = ACTIVE "N" if(NAME == BOOTFS) ACTIVE = ACTIVE "R" if(! ACTIVE) ACTIVE = "-" for(J = 1; J <= length(FSNAMES); J++) { FSNAME = FSNAMES[J] if(FSNAME ~ "^" BENAME_BEGINS_WITH "/" BENAME "(/[^@]*)?$") { if((BENAME_BEGINS_WITH "/" BENAME) == FSNAME) { MOUNTPOINT = MOUNTS[FSNAME] if(! MOUNTPOINT) MOUNTPOINT = "-" CREATION = __convert_date(CREATIONS[FSNAME]) } ORIGIN = ORIGINS[FSNAME] if(ORIGIN == "-") SPACE += SPACES[FSNAME] else { if(OPTION_D == 1) SPACE += REFER[FSNAME] else SPACE += SPACES[FSNAME] + SPACES[ORIGIN] } } } if(OPTION_H == 1) printf "%s\t%s\t%s\t%s\t%s\n", BENAME, ACTIVE, MOUNTPOINT, __show_units(SPACE), CREATION else printf "%-" FSNAME_LENGTH "s %-6s %-" MOUNTPOINT_LENGTH "s %6s %s\n", BENAME, ACTIVE, MOUNTPOINT, __show_units(SPACE), CREATION } } }' 2> /dev/null ;; (create) # ------------------------------------------------------------------ case ${#} in (4) if ! [ ${2} = "-e" ] then __usage fi # check if argument for -e option is full path dataset # argument for -e option must be 'beName' or 'beName@snapshot' if echo ${3} | grep -q "/" 2> /dev/null then __usage fi __be_exist ${POOL}/${BEDS}/${3} if zfs list -H -o name ${POOL}/${BEDS}/${4} 1> /dev/null 2> /dev/null then echo "ERROR: Boot environment '${4}' already exists" exit 1 fi __be_new ${POOL}/${BEDS}/${3} ${POOL}/${BEDS}/${4} ;; (2) if __be_snapshot ${2} then if ! zfs snapshot -r ${POOL}/${BEDS}/${2} 1> /dev/null 2> /dev/null then echo "ERROR: Cannot create '${2}' recursive snapshot" exit 1 fi else __be_new ${ROOTFS} ${POOL}/${BEDS}/${2} fi ;; (*) __usage ;; esac echo "Created successfully" ;; (activate) # ---------------------------------------------------------------- if [ ${#} -ne 2 ] then __usage fi __be_exist ${POOL}/${BEDS}/${2} if [ "${BOOTFS}" = "${POOL}/${BEDS}/${2}" ] then echo "Already activated" exit 0 else if __be_mounted ${POOL}/${BEDS}/${2} then MNT=$( mount | grep -E "^${POOL}/${BEDS}/${2} " | awk '{print $3}' ) if [ "${MNT}" != "/" ] then # boot environment is not current root and its mounted echo "Attempt to unmount boot environment '${2}' mounted at '${MNT}'" if ! umount ${MNT} 1> /dev/null 2> /dev/null then echo "ERROR: Unable to unmount boot environment '${2}' mounted at '${MNT}'" echo "ERROR: Cannot activate manually mounted boot environment '${2}'" exit 1 fi echo "Gracefully unmounted boot environment '${2}' from '${MNT}' mount point" fi fi # do not change root (/) mounted boot environment mountpoint HAVE_ZFSBE=0 if [ "${ROOTFS}" != "${POOL}/${BEDS}/${2}" ] then TMPMNT=$( mktemp -d -t BE-${2} ) if ! mkdir -p ${TMPMNT} 2> /dev/null then echo "ERROR: Cannot create '${TMPMNT}' directory" exit 1 fi MOUNT=0 while read FS MNT TYPE OPTS DUMP FSCK; do if [ "${FS}" = "${POOL}/${BEDS}/${2}" ] then MOUNT=${MNT} break fi done << EOF $( mount -p ) EOF if [ ${MOUNT} -eq 0 ] then zfs set canmount=noauto ${POOL}/${BEDS}/${2} zfs set mountpoint=${TMPMNT} ${POOL}/${BEDS}/${2} zfs mount ${POOL}/${BEDS}/${2} else TMPMNT=${MOUNT} fi if [ -f ${TMPMNT}/etc/rc.d/zfsbe ]; then HAVE_ZFSBE=1 fi if [ -f /boot/zfs/zpool.cache ] then if [ -z "${BOOTPATH}" ] then cp /boot/zfs/zpool.cache ${TMPMNT}/boot/zfs/zpool.cache fi else rm -f ${TMPMNT}/boot/zfs/zpool.cache fi LOADER_CONFIGS=${TMPMNT}/boot/loader.conf if [ -f ${TMPMNT}/boot/loader.conf.local ] then LOADER_CONFIGS="${LOADER_CONFIGS} ${TMPMNT}/boot/loader.conf.local" else if [ -n "${BOOTPATH}" ] then LOADER_CONFIGS=${BOOTPATH}/boot/loader.conf if [ -f ${BOOTPATH}/boot/loader.conf.local ] then LOADER_CONFIGS="${LOADER_CONFIGS} ${BOOTPATH}/boot/loader.conf.local" fi fi fi sed -i '' -E s/"^vfs.root.mountfrom=.*$"/"vfs.root.mountfrom=\"zfs:${POOL}\/${BEDS}\/${2##*/}\""/g ${LOADER_CONFIGS} if [ ${MOUNT} -eq 0 ] then zfs umount ${POOL}/${BEDS}/${2} zfs set mountpoint=/ ${POOL}/${BEDS}/${2} fi fi if ! zpool set bootfs=${POOL}/${BEDS}/${2} ${POOL} 1> /dev/null 2> /dev/null then echo "ERROR: Failed to activate '${2}' boot environment" exit 1 fi fi # execute ZFS LIST only once ZFS_LIST=$( zfs list -t filesystem -H -o name -r ${POOL}/${BEDS} ) # disable automatic mount on all inactive boot environments echo "${ZFS_LIST}" \ | grep -v "^${POOL}/${BEDS}$" \ | grep -v "^${POOL}/${BEDS}/${2}$" \ | grep -v "^${POOL}/${BEDS}/${2}/" \ | while read NAME do zfs set canmount=noauto ${NAME} done # enable automatic mount for active boot environment and promote it if [ ${HAVE_ZFSBE} -eq 1 ]; then ZFSBE_CANMOUNT=noauto else ZFSBE_CANMOUNT=on fi echo "${ZFS_LIST}" \ | grep -E "^${POOL}/${BEDS}/${2}(/|$)" \ | while read NAME do zfs set canmount=${ZFSBE_CANMOUNT} ${NAME} while __be_clone ${NAME} do zfs promote ${NAME} done done # check if we need to update grub if [ "${GRUB}" = YES ] then __update_grub fi # allow reboot -r to change to new root filesystem kenv vfs.root.mountfrom="zfs:${POOL}/${BEDS}/${2}" 1>/dev/null 2>/dev/null echo "Activated successfully" ;; (destroy) # ----------------------------------------------------------------- if [ "${2}" != "-F" ] then DESTROY=${2} else DESTROY=${3} fi __be_exist ${POOL}/${BEDS}/${DESTROY} case ${#} in (2) echo "Are you sure you want to destroy '${2}'?" echo -n "This action cannot be undone (y/[n]): " read CHOICE ;; (3) if [ "${2}" != "-F" ] then __usage fi CHOICE=Y ;; (*) __usage ;; esac if [ "${BOOTFS}" = "${POOL}/${BEDS}/${DESTROY}" ] then echo "ERROR: Cannot destroy active boot environment" exit 1 fi case ${CHOICE} in (Y|y|[Yy][Ee][Ss]) # destroy snapshot or boot environment if __be_snapshot ${POOL}/${BEDS}/${DESTROY} then # destroy desired snapshot if ! zfs destroy -r ${POOL}/${BEDS}/${DESTROY} 1> /dev/null 2> /dev/null then echo "ERROR: Snapshot '${2}' is origin for other boot environment" exit 1 fi else if __be_clone ${POOL}/${BEDS}/${DESTROY} then # promote clones dependent on snapshots used by destroyed boot environment zfs list -H -t filesystem,snapshot,volume -o name,origin -r ${POOL} \ | while read NAME ORIGIN do if echo "${ORIGIN}" | grep -q -E "${POOL}/${BEDS}/${DESTROY}(/.*@|@)" 2> /dev/null then zfs promote ${NAME} fi done # get origins used by destroyed boot environment ORIGIN_SNAPSHOTS=$( zfs list -H -t filesystem,snapshot,volume -o origin -r ${POOL}/${BEDS}/${DESTROY} | grep -v '^-$' | awk -F "@" '{print $2}' | sort -u ) fi # check if boot environment was created from existing snapshot ORIGIN=$( zfs list -H -o origin ${POOL}/${BEDS}/${DESTROY} ) CREATION=$( zfs list -H -o creation ${POOL}/${BEDS}/${DESTROY} ) CREATION=$( date -j -f "%a %b %d %H:%M %Y" "${CREATION}" +"%Y-%m-%d-%H:%M" ) SNAPSHOT_NAME=$( echo "${ORIGIN}" | cut -d '@' -f 2 | sed -E 's/:[0-9]{2}$//g' ) if [ "${2}" = "-F" ] then CHOICE=1 elif [ "${SNAPSHOT_NAME}" != "${CREATION}" ] then ORIGIN=$( basename ${ORIGIN} ) echo "Boot environment '${DESTROY}' was created from existing snapshot" echo -n "Destroy '${ORIGIN}' snapshot? (y/[n]): " read CHOICE case ${CHOICE} in (Y|y|[Yy][Ee][Ss]) CHOICE=1 ;; (*) CHOICE=0 echo "Origin snapshot '${ORIGIN}' will be preserved" ;; esac else CHOICE=1 fi # destroy boot environment zfs destroy -r ${POOL}/${BEDS}/${DESTROY} # check if boot environment is clone if __be_clone ${POOL}/${BEDS}/${DESTROY} then # promote datasets dependent on origins used by destroyed boot environment ALL_ORIGINS=$( zfs list -H -t filesystem,snapshot,volume -o name,origin -r ${POOL} ) echo "${ORIGIN_SNAPSHOTS}" \ | while read S do echo "${ALL_ORIGINS}" \ | grep "${S}" \ | awk '{print $1}' \ | while read I do zfs promote ${I} done done fi # destroy origins used by destroyed boot environment SNAPSHOTS=$( zfs list -H -t snapshot -o name -r ${POOL} ) echo "${ORIGIN_SNAPSHOTS}" \ | while read S do echo "${SNAPSHOTS}" \ | grep "@${S}$" \ | while read I do if [ ${CHOICE} -eq 1 ] then zfs destroy ${I} fi done done fi # check if we need to update grub if [ "${GRUB}" = YES ] then __update_grub fi echo "Destroyed successfully" ;; (*) echo "Boot environment '${DESTROY}' has not been destroyed" ;; esac ;; (export) # ----------------------------------------------------------------- if [ "${#}" -eq 2 ] then OPTS= ITEM=${2} elif [ "${#}" -eq 3 -a "${2}" = "-v" ] then OPTS=${2} ITEM=${3} else __usage fi __be_exist ${POOL}/${BEDS}/${ITEM} # always take recent snapshot before export FMT=$( date "+%Y-%m-%d-%H:%M:%S" ) if ! zfs snapshot -r ${POOL}/${BEDS}/${ITEM}@${FMT} 1> /dev/null 2> /dev/null then echo "ERROR: Cannot create '${ITEM}' snapshot" exit 1 fi # send piped/redirected zfs data stream zfs send ${OPTS} ${POOL}/${BEDS}/${ITEM}@${FMT} ;; (import) # ----------------------------------------------------------------- if [ "${#}" -eq 2 ] then OPTS= ITEM=${2} elif [ "${#}" -eq 3 -a "${2}" = "-v" ] then OPTS=${2} ITEM=${3} else __usage fi if zfs list -H -o name ${POOL}/${BEDS}/${ITEM} 1> /dev/null 2> /dev/null then echo "ERROR: Boot environment '${ITEM}' already exists" exit 1 fi # receive piped/redirected zfs data stream zfs recv ${OPTS} ${POOL}/${BEDS}/${ITEM} ;; (rename) # ------------------------------------------------------------------ if [ ${#} -ne 3 ] then __usage fi __be_exist ${POOL}/${BEDS}/${2} if zfs list -H -o name ${POOL}/${BEDS}/${3} 2> /dev/null then echo "ERROR: Boot environment '${3}' already exists" exit 1 fi zfs rename -u ${POOL}/${BEDS}/${2} ${POOL}/${BEDS}/${3} # check if we need to update loader config if [ "${BOOTFS}" = "${POOL}/${BEDS}/${2}" ] then LOADER_CONFIGS=/boot/loader.conf if [ -f /boot/loader.conf.local ] then LOADER_CONFIGS="${LOADER_CONFIGS} /boot/loader.conf.local" fi sed -i '' -E s@"^vfs.root.mountfrom=.*$"@"vfs.root.mountfrom=\"zfs:${POOL}\/${BEDS}\/${3##*/}\""@g ${LOADER_CONFIGS} fi # check if we need to update grub if [ "${GRUB}" = YES ] then __update_grub fi echo "Renamed successfully" ;; (mount) # ------------------------------------------------------------ if [ ${#} -eq 2 ] then TARGET=$( env TMPDIR=/var/tmp mktemp -d -t BE-${2} ) elif [ ${#} -eq 3 ] then TARGET=${3} else __usage fi __be_exist ${POOL}/${BEDS}/${2} if __be_mounted ${POOL}/${BEDS}/${2} then MNT=$( mount | grep -E "^${POOL}/${BEDS}/${2} " | awk '{print $3}' ) echo "Boot environment '${2}' is already mounted at '${MNT}'" exit 1 fi if ! mkdir -p ${TARGET} 2> /dev/null then echo "ERROR: Cannot create '${TARGET}' mountpoint" exit 1 fi if ! mount -t zfs ${POOL}/${BEDS}/${2} ${TARGET} then echo "ERROR: Cannot mount '${2}' at '${TARGET}' mountpoint" exit 1 fi zfs list -H -o name,mountpoint -r ${POOL}/${BEDS}/${2} \ | grep -v -E "[[:space:]](legacy|none)$" \ | sort -n \ | grep -E "^${POOL}/${BEDS}/${2}/" \ | while read FS MOUNTPOINT do if [ "{FS}" != "${POOL}/${BEDS}/${2}" ] then INHERIT=$( zfs get -H -o source mountpoint ${FS} ) if [ "${INHERIT}" = "local" ] then case ${MOUNTPOINT} in (legacy|none) continue ;; (*) MOUNTPOINT="/$( echo "${FS}" | sed s^"${POOL}/${BEDS}/${2}/"^^g )" ;; esac fi fi if ! mkdir -p ${TARGET}${MOUNTPOINT} 1> /dev/null 2> /dev/null then echo "ERROR: Cannot create '${TARGET}${MOUNTPOINT}' mountpoint" exit 1 fi if ! mount -t zfs ${FS} ${TARGET}${MOUNTPOINT} 1> /dev/null 2> /dev/null then echo "ERROR: Cannot mount '${FS}' at '${TARGET}${MOUNTPOINT}' mountpoint" exit 1 fi done echo "Mounted successfully on '${TARGET}'" ;; (umount|unmount) # ---------------------------------------------------------- if [ ${#} -eq 2 ] then # we need this empty section for argument checking : elif [ ${#} -eq 3 -a "${2}" = "-f" ] then OPTS="-f" shift else __usage fi __be_exist ${POOL}/${BEDS}/${2} if ! __be_mounted ${POOL}/${BEDS}/${2} then echo "Boot environment '${2}' is not mounted" exit 1 fi MOUNT=$( mount ) MOUNTPOINT=$( echo "${MOUNT}" | grep -m 1 "^${POOL}/${BEDS}/${2} on " | awk '{print $3}' ) echo "${MOUNT}" \ | awk '{print $1}' \ | grep -E "^${POOL}/${BEDS}/${2}(/|$)" \ | sort -n -r \ | while read FS do if ! umount ${OPTS} ${FS} 1> /dev/null 2> /dev/null then echo "ERROR: Cannot umount '${FS}' dataset" exit 1 fi done echo "Unmounted successfully" # only delete temporary mountpoint directory if echo "${MOUNTPOINT}" | grep -q -E "/BE-${2}\.[a-zA-Z0-9]{8}" 1> /dev/null 2> /dev/null then # delete only when it is empty directory if [ $( find ${MOUNTPOINT} | head | wc -l | bc ) -eq 1 ] then rm -r "${MOUNTPOINT}" fi fi ;; (chroot) # ---------------------------------------------------------- if [ ${#} -ne 2 ] then __usage fi __be_exist ${POOL}/${BEDS}/${2} if ! __be_mounted ${POOL}/${BEDS}/${2} then "${0}" mount "${2}" 1> /dev/null 2> /dev/null fi MOUNT=$( mount ) MOUNTPOINT=$( echo "${MOUNT}" | grep -m 1 "^${POOL}/${BEDS}/${2} on " | awk '{print $3}' ) echo "Entered chroot(8) for '${2}' boot environment" chroot "${MOUNTPOINT}" "${0}" umount -f "${2}" 1> /dev/null 2> /dev/null echo "Leaved from chroot(8) for '${2}' boot environment" ;; (reroot) # ----------------------------------------------------------------- # activate new BE which would also set new root filesystem using kenv(1) ${0} activate ${2} # reroot to new root with reboot(8) command reboot -r # no need to display messages here "because Kansas is going bye bye" ;; (*) # ----------------------------------------------------------------------- __usage ;; esac