#!/bin/sh -e # Copyright 2012 Slawomir Wojciech Wojtczak (vermaden). All rights reserved. # Copyright 2012 Bryan Drewery (bdrewery). 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:/sbin:/usr/sbin if [ $( uname -r | cut -d '.' -f1 ) -lt 8 ] then echo "ERROR: beadm works on FreeBSD 8.0 or later" exit 1 fi __usage() { local NAME=${0##*/} echo "usage:" echo " ${NAME} subcommand cmd_options" echo echo " subcommands:" echo echo " ${NAME} activate beName" echo " ${NAME} create [-e nonActiveBe | -e beName@snapshot] beName" echo " ${NAME} create beName@snapshot" echo " ${NAME} destroy [-F] beName | beName@snapshot" echo " ${NAME} list" echo " ${NAME} mount" echo " ${NAME} mount beName [mountpoint]" echo " ${NAME} umount | unmount [-f] beName" echo " ${NAME} rename origBeName newBeName" exit 1 } # check if BE 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 a snapshot __be_snapshot() { # 1=DATASET/SNAPSHOT echo "${1}" | grep -q "@" } # create new BE __be_new() { # 1=SOURCE 2=TARGET local SOURCE=$( echo ${1} | cut -d '@' -f 1 ) if __be_snapshot ${1} then 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 exists" exit 1 fi done else if zfs list -H -o name ${1}@${2##*/} 1> /dev/null 2> /dev/null then echo "ERROR: Snapshot '${1}@${2##*/}' already exists" exit 1 fi ### if ! zfs snapshot -r ${1}@${2##*/} 1> /dev/null 2> /dev/null # old # FMT=$( date "+%Y-%m-%d-%H:%M:%S" ) # NEW # if ! zfs snapshot -r ${1}@${FMT} 1> /dev/null 2> /dev/null # NEW # then ##### echo "ERROR: Cannot create snapshot '${1}@${2##*/}'" # old # echo "ERROR: Cannot create snapshot '${1}@${FMT}'" # NEW # exit 1 fi fi zfs list -H -o name -r ${SOURCE} \ | while read FS do local OPTS="" while read NAME PROPERTY VALUE do local OPTS="-o ${PROPERTY}=${VALUE} ${OPTS}" done << EOF $( zfs get -o name,property,value -s local,received -H all ${FS} | grep -v -E "(canmount)" ) EOF DATASET=$( echo ${FS} | awk '{print $1}' | sed -E s/"^${POOL}\/ROOT\/${SOURCE##*/}"/"${POOL}\/ROOT\/${2##*/}"/g ) if [ "${OPTS}" = "-o = " ] then local OPTS="" fi if __be_snapshot ${1} then zfs clone -o canmount=off ${OPTS} ${FS}@${1##*@} ${DATASET} else ######### zfs clone -o canmount=off ${OPTS} ${FS}@${2##*/} ${DATASET} # old # zfs clone -o canmount=off ${OPTS} ${FS}@${FMT} ${DATASET} # NEW # fi done echo "Created successfully" } ROOTFS=$( mount | awk '/ \/ / {print $1}' ) if echo ${ROOTFS} | grep -q -E "^/dev/" then echo "ERROR: This system does not boot from ZFS pool" exit 1 fi POOL=$( echo ${ROOTFS} | awk -F '/' '{print $1}' ) if ! zfs list ${POOL}/ROOT 1> /dev/null 2> /dev/null then echo "ERROR: This system is not configured for boot environments" exit 1 fi BOOTFS=$( zpool list -H -o bootfs ${POOL} ) case ${1} in (list) # -------------------------------------------------------------------- if [ ${#} -ne 1 ] then __usage fi BENAME_STARTS_WITH="${POOL}/ROOT" LIST=$( zfs list -o name,mountpoint,creation -s creation -H -d 1 -r ${POOL}/ROOT | grep -E "^${POOL}/ROOT/" ) WIDTH_CREATION=$( echo "${LIST}" | awk '{print $5}' | wc -L ) WIDTH_NAME=$( echo "${LIST}" | awk '{print $1}' | wc -L ) WIDTH_NAME=$(( ${WIDTH_NAME} - ${#BENAME_STARTS_WITH} - 1 )) printf "%-${WIDTH_NAME}s %-6s %-10s %6s %6s %s\n" \ BE Active Mountpoint Space Policy Created echo "${LIST}" \ | while read NAME MOUNTPOINT Y m d H M do TOTAL=0 NAME=${NAME##*/} unset ACTIVE if [ "${BENAME_STARTS_WITH}/${NAME}" = "${ROOTFS}" ] then ACTIVE="${ACTIVE}N" fi if [ "${BENAME_STARTS_WITH}/${NAME}" = "${BOOTFS}" ] then ACTIVE="${ACTIVE}R" fi if [ -z "${ACTIVE}" ] then ACTIVE="-" fi printf "%-${WIDTH_NAME}s %-6s " ${NAME} ${ACTIVE} case ${ACTIVE} in (N|NR) MOUNT="/" ;; (*) MOUNT="-" ;; esac while read I do USED=$( zfs list -H -o used ${I} ) if [ ${USED} = "0" ] then continue fi case "${USED}" in (*K) SIZE=$( echo "${USED} * 1000 " | tr -c -d '\[0-9]* .\n' | bc -l ) ;; (*M) SIZE=$( echo "${USED} * 1000000 " | tr -c -d '\[0-9]* .\n' | bc -l ) ;; (*G) SIZE=$( echo "${USED} * 1000000000 " | tr -c -d '\[0-9]* .\n' | bc -l ) ;; (*T) SIZE=$( echo "${USED} * 1000000000000 " | tr -c -d '\[0-9]* .\n' | bc -l ) ;; (*P) SIZE=$( echo "${USED} * 1000000000000000 " | tr -c -d '\[0-9]* .\n' | bc -l ) ;; (*E) SIZE=$( echo "${USED} * 1000000000000000000 " | tr -c -d '\[0-9]* .\n' | bc -l ) ;; (*Z) SIZE=$( echo "${USED} * 1000000000000000000000" | tr -c -d '\[0-9]* .\n' | bc -l ) ;; (*) SIZE="${USED}" ;; esac TOTAL=$( echo "${SIZE} + ${TOTAL}" | bc -l ) done << EOF $( zfs list -H -t all -o name,origin -r "${BENAME_STARTS_WITH}/${NAME}" | tr '\t' '\n' | grep -v -E "^-$" ) EOF RANGE=$( echo ${TOTAL} | cut -d . -f 1 ) case $( echo "${RANGE}" | wc -c | tr -c -d '[0-9]\n' ) in (5|6|7) TOTAL="$( echo ${TOTAL} / 1000 | bc -l | grep -o -E "[0-9]*\.[0-9]{1}" )K" ;; (8|9|10) TOTAL="$( echo ${TOTAL} / 1000000 | bc -l | grep -o -E "[0-9]*\.[0-9]{1}" )M" ;; (11|12|13) TOTAL="$( echo ${TOTAL} / 1000000000 | bc -l | grep -o -E "[0-9]*\.[0-9]{1}" )G" ;; (14|15|16) TOTAL="$( echo ${TOTAL} / 1000000000000 | bc -l | grep -o -E "[0-9]*\.[0-9]{1}" )T" ;; (17|18|19) TOTAL="$( echo ${TOTAL} / 1000000000000000 | bc -l | grep -o -E "[0-9]*\.[0-9]{1}" )P" ;; (20|21|22) TOTAL="$( echo ${TOTAL} / 1000000000000000000 | bc -l | grep -o -E "[0-9]*\.[0-9]{1}" )E" ;; (23|24|25) TOTAL="$( echo ${TOTAL} / 1000000000000000000000 | bc -l | grep -o -E "[0-9]*\.[0-9]{1}" )Z" ;; esac printf "%-10s %6s %-6s " ${MOUNT} ${TOTAL} "static" date -j -f "%a %b %d %H:%M %Y" "${Y} ${m} ${d} ${H} ${M}" +"%Y-%m-%d %H:%M" done ####### LIST=$( zfs list -o name,used,mountpoint,creation -s creation -H -d 1 -r ${POOL}/ROOT | grep -E "^${POOL}/ROOT/" ) ####### WIDTH_CREATION=$( echo "${LIST}" | awk '{print $5}' | wc -L ) ####### WIDTH_NAME=$( echo "${LIST}" | awk '{print $1}' | wc -L ) ####### WIDTH_NAME=$(( ${WIDTH_NAME} - ${#BENAME_STARTS_WITH} - 1 )) ####### printf "%-${WIDTH_NAME}s %-6s %-10s %5s %6s %s\n" \ ####### BE Active Mountpoint Space Policy Created ####### echo "${LIST}" \ ####### | while read NAME USED MOUNTPOINT Y m d H M ####### do ####### NAME=${NAME##*/} ####### unset ACTIVE ####### if [ "${BENAME_STARTS_WITH}/${NAME}" = "${ROOTFS}" ] ####### then ####### ACTIVE="${ACTIVE}N" ####### fi ####### if [ "${BENAME_STARTS_WITH}/${NAME}" = "${BOOTFS}" ] ####### then ####### ACTIVE="${ACTIVE}R" ####### fi ####### if [ -z "${ACTIVE}" ] ####### then ####### ACTIVE="-" ####### fi ####### printf "%-${WIDTH_NAME}s %-6s " ${NAME} ${ACTIVE} ####### case ${ACTIVE} in ####### (N|NR) MOUNT="/" ;; ####### (*) MOUNT="-" ;; ####### esac ####### printf "%-10s %5s %-6s " ${MOUNT} ${USED} "static" ####### date -j -f "%a %b %d %H:%M %Y" "${Y} ${m} ${d} ${H} ${M}" +"%Y-%m-%d %H:%M" ####### done ;; (create) # ------------------------------------------------------------------ case ${#} in (4) if ! [ ${2} = "-e" ] then __usage fi __be_exist ${POOL}/ROOT/${3} if zfs list -H -o name ${POOL}/ROOT/${4} 1> /dev/null 2> /dev/null then echo "ERROR: Boot environment '${4}' already exists" exit 1 fi __be_new ${POOL}/ROOT/${3} ${POOL}/ROOT/${4} ;; (2) if __be_snapshot ${2} then if ! zfs snapshot -r ${POOL}/ROOT/${2} 1> /dev/null 2> /dev/null then echo "ERROR: Cannot create '${2}' recursive snapshot" exit 1 fi echo "Created successfully" else __be_new ${ROOTFS} ${POOL}/ROOT/${2} fi ;; (*) __usage ;; esac ;; (activate) # ---------------------------------------------------------------- if [ ${#} -ne 2 ] then __usage fi __be_exist ${POOL}/ROOT/${2} if [ "${BOOTFS}" = "${POOL}/ROOT/${2}" ] then echo "Already activated" exit 0 else if mount | grep -E "^${POOL}/ROOT/${2} " 1> /dev/null 2> /dev/null then MNT=$( mount | grep -E "^${POOL}/ROOT/${2} " | awk '{print $3}' ) if [ "${MNT}" != "/" ] then echo "ERROR: The '${2}' is mounted at '${MNT}'" echo "ERROR: Cannot activate mounted boot environment" exit 1 fi fi if [ "${ROOTFS}" != "${POOL}/ROOT/${2}" ] then TMPMNT="/tmp/BE" if ! mkdir -p ${TMPMNT} then echo "ERROR: Cannot create '${TMPMNT}' directory" exit 1 fi MOUNT=0 while read FS MNT do if [ "${FS}" = "${POOL}/ROOT/${2}" ] then MOUNT=${MNT} fi done << EOF $( mount -p | awk '{print $1 " " $2}' ) EOF if [ ${MOUNT} -eq 0 ] then zfs set canmount=noauto ${POOL}/ROOT/${2} zfs set mountpoint=${TMPMNT} ${POOL}/ROOT/${2} zfs mount ${POOL}/ROOT/${2} 1> /dev/null 2> /dev/null else TMPMNT=${MOUNT} fi cp /boot/zfs/zpool.cache ${TMPMNT}/boot/zfs/zpool.cache LOADER_CONFIGS=${TMPMNT}/boot/loader.conf if [ -f ${TMPMNT}/boot/loader.conf.local ] then LOADER_CONFIGS="${LOADER_CONFIGS} ${TMPMNT}/boot/loader.conf.local" fi sed -i '' -E s/"^vfs.root.mountfrom=.*$"/"vfs.root.mountfrom=\"zfs:${POOL}\/ROOT\/${2##*/}\""/g ${LOADER_CONFIGS} 2> /dev/null if [ ${MOUNT} -eq 0 ] then zfs umount ${POOL}/ROOT/${2} zfs set mountpoint=legacy ${POOL}/ROOT/${2} fi fi if ! zpool set bootfs=${POOL}/ROOT/${2} ${POOL} 2> /dev/null then echo "ERROR: Failed to activate '${POOL}/ROOT/${2}'" exit 1 fi fi # disable automatic mount on all inactive datasets zfs list -H -o name -r ${POOL}/ROOT \ | grep -v "${POOL}/ROOT/${2}" \ | while read I do zfs set canmount=noauto ${I} done # enable automatic mount for active BE and promote it zfs list -H -o name,origin -t filesystem -r ${POOL}/ROOT/${2} \ | while read I ORIGIN do zfs set canmount=on ${I} if [ ${ORIGIN} != "-" ] then zfs promote ${I} fi done echo "Activated successfully" ;; (destroy) # ----------------------------------------------------------------- case ${#} in (2) echo "Are you sure you want to destroy '${2}'?" echo -n "This action cannot be undone (y/[n]): " read CHOICE DESTROY=${2} ;; (3) if [ "${2}" != "-F" ] then __usage fi CHOICE=Y DESTROY=${3} ;; (*) __usage ;; esac __be_exist ${POOL}/ROOT/${DESTROY} if [ "${BOOTFS}" = "${POOL}/ROOT/${DESTROY}" ] then echo "ERROR: '${POOL}/ROOT/${2}' is current active boot environment" exit 1 fi case ${CHOICE} in (Y|y|[Yy][Ee][Ss]) # delete snapshot or delete boot environment if __be_snapshot ${POOL}/ROOT/${DESTROY} then # destroy desired snapshot if ! zfs destroy -r ${POOL}/ROOT/${DESTROY} 1> /dev/null 2> /dev/null then echo "ERROR: Snapshot '${2}' is origin for other boot environment(s)" exit 1 fi else # promote clones dependent on snapshots used by destroyed boot environment zfs list -H -t all -o name,origin \ | while read NAME ORIGIN do if echo "${ORIGIN}" | grep -q -E "${POOL}/ROOT/${DESTROY}(/.*@|@)" 2> /dev/null then zfs promote ${NAME} fi done # get origins used by destroyed boot environment ORIGINS=$( zfs list -H -t all -o origin -r ${POOL}/ROOT/${DESTROY} ) # destroy boot environment zfs destroy -r ${POOL}/ROOT/${DESTROY} # destroy origins used by destroyed boot environment echo "${ORIGINS}" \ | while read I do zfs destroy ${I} done ############## # destroy desired boot environment ############## zfs list -r -H -o name ${POOL}/ROOT/${DESTROY} \ ############## | sort -n -r \ ############## | while read DATASET ############## do ############## zfs umount ${DATASET} 1> /dev/null 2> /dev/null || true ############## done ############## # get origins that needs to be deleted ############## ORIGINS=$( zfs list -r -H -o origin ${POOL}/ROOT/${DESTROY} ) ############## # promote dependent clones ############## zfs list -H -o name,origin \ ############## | awk -v destroy=${POOL}/ROOT/${DESTROY} '$2 ~ destroy {print $1}' \ ############## | sort -r \ ############## | while read I ############## do ############## echo PROMOTE 1: $I ############## zfs promote ${I} 2> /dev/null ############## done ############## # destroy desired boot environment ############## if ! zfs destroy ${POOL}/ROOT/${DESTROY} 1> /dev/null 2> /dev/null ############## then ############## # promote dependent clones when needed ############## zfs destroy -r ${POOL}/ROOT/${DESTROY} 2>&1 \ ############## | grep "${POOL}/ROOT/" \ ############## | grep -v "@" \ ############## | while read I ############## do ############## echo PROMOTE 2: $I ############## zfs promote ${I} 2> /dev/null ############## done ############## fi ############## # destroy origins that needs to be deleted ############## if [ "${ORIGINS}" != "-" ] ############## then ############## echo "${ORIGINS}" \ ############## | while read I ############## do ############## zfs destroy -r ${I} 2> /dev/null || true ############## done ############## fi fi echo "Destroyed successfully" ;; (*) echo "'${DESTROY}' has not been destroyed" ;; esac ;; (rename) # ------------------------------------------------------------------ if [ ${#} -ne 3 ] then __usage fi __be_exist ${POOL}/ROOT/${2} if [ "${BOOTFS}" = "${POOL}/ROOT/${2}" ] then echo "ERROR: Renaming the active BE is not supported" exit 1 fi if zfs list -H -o name ${POOL}/ROOT/${3} 2> /dev/null then echo "ERROR: Boot environment '${3}' already exists" exit 1 fi zfs rename ${POOL}/ROOT/${2} ${POOL}/ROOT/${3} echo "Renamed successfully" ;; (mount) # ------------------------------------------------------------------- COMPLETE=1 if [ ${#} -eq 1 -o ${#} -eq 2 ] then if [ ${2} ] then BE=${2} else zfs list -H -o name -d 1 -r ${POOL}/ROOT \ | grep "${POOL}/ROOT/" \ | while read NAME do NAME=${NAME##*/} if mount | grep -E "^${POOL}/ROOT/${NAME}" 1> /dev/null 2> /dev/null then echo ${NAME} mount \ | grep -E "^${POOL}/ROOT/${NAME}" \ | awk '{print $1 " " $3}' \ | column -t \ | awk '{print " " $0}' echo fi done exit 0 fi elif [ ${#} -eq 3 ] then BE=${2} TARGET=${3} else __usage fi __be_exist "${POOL}/ROOT/${BE}" if mount | grep -E "^${POOL}/ROOT/${BE} " 1> /dev/null 2> /dev/null then MNT=$( mount | grep -E "^${POOL}/ROOT/${BE} " | awk '{print $3}' ) echo "The '${BE}' is already mounted at '${MNT}'" exit 1 fi if ! [ ${3} ] then TARGET=$( mktemp -d /tmp/tmp.XXXXXX ) mkdir -p ${TARGET} fi if ! mount -t zfs ${POOL}/ROOT/${BE} ${TARGET} then echo "ERROR: Cannot mount '${POOL}/ROOT/${BE}' at '${TARGET}' mountpoint" exit 1 fi PREFIX=$( echo ${POOL}/ROOT/${BE}/ | sed 's/\//\\\//g' ) zfs list -H -o name,mountpoint -r ${POOL}/ROOT/${BE} \ | grep -v "legacy$" \ | sort -n \ | grep -E "^${POOL}/ROOT/${BE}/" \ | while read FS MOUNTPOINT do if [ "{FS}" != "${POOL}/ROOT/${BE}" ] then INHERIT=$( zfs get -H -o source mountpoint ${FS} ) if [ "${INHERIT}" = "local" ] then if [ "${MOUNTPOINT}" = "legacy" ] then continue else MOUNTPOINT=$( echo "${FS}" | sed s/"${PREFIX}"//g ) MOUNTPOINT="/${MOUNTPOINT}" fi 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" COMPLETE=0 fi done if [ ${COMPLETE} -eq 0 ] then echo "Partially mounted" else echo "Mounted successfully on '${TARGET}'" fi ;; (umount|unmount) # ------------------------------------------------------------------ COMPLETE=1 if [ ${#} -eq 2 ] then BE=${2} elif [ ${#} -eq 3 ] then BE=${3} if [ "${2}" = "-f" ] then OPTS="-f" else __usage fi else __usage fi __be_exist "$POOL/ROOT/$BE" if ! mount | grep -E "^${POOL}/ROOT/${BE} " 1> /dev/null 2> /dev/null then echo "The '${BE}' is not mounted" exit 1 fi mount \ | grep "^${POOL}/ROOT/${BE}" \ | awk '{print $1}' \ | sort -n -r \ | while read FS do if ! umount ${OPTS} ${FS} 1> /dev/null 2> /dev/null then echo "ERROR: Cannot umount '${FS}' dataset" COMPLETE=0 fi done if [ ${COMPLETE} -eq 0 ] then echo "Partially unmounted" else echo "Unmounted successfully" fi ;; (*) # ----------------------------------------------------------------------- __usage ;; esac