#!/bin/sh # # Copyright (c) 2017-2018 Aaron LI # MIT License # # Tool to update a DragonFly BSD system using binary releases or # snapshot builds. # # Exit immediately if any untested command fails in non-interactive mode set -e # Exit with an error when an undefined variable is referenced set -u DEBUG=${DEBUG:-""} VERSION="0.1.5" NAME="dfly-update" TOOLDIR="${0%/*}" PREFIX="${PREFIX:-${TOOLDIR}}" CONFIGFILE="${PREFIX}/${NAME}.conf" # # Error Codes # EC_OS=10 EC_TMPFILE=11 EC_MD5=12 EC_ARGS=13 EC_FETCH=14 EC_MOUNT=15 EC_UMOUNT=16 EC_VN=17 EC_TAR=18 EC_MTREE=19 EC_CPDUP=20 EC_NOFILE=21 # # Default Configurations # # Temporary directory to cache the image, etc, ... CACHE_DIR="/var/tmp/${NAME}" # Directory to mount the system image MNT_DIR="/mnt/${NAME}" # Backup directory BACK_DIR="/var/backups/${NAME}" # Ignored files to be kept from overriding by the upgrade. FILES_IGNORE=' /boot/loader.conf /etc/crypttab /etc/fstab /etc/group /etc/localtime /etc/login.conf.db /etc/master.passwd /etc/passwd /etc/pwd.db /etc/rc.conf /etc/spwd.db ' # Filename suffix for the temporarily backup files to avoid overriding. BAK_SUF="__bak__" # Filename suffix for newly installed config files that need (manual) # merge. NEW_SUF="__new__" # # Helper Functions # debug() { # Add "|| true" to work with "set -e" [ -n "${DEBUG}" ] && echo "DEBUG: $@" >&2 || true } log() { echo "$@" >&2 } warn() { echo "WARNING: $@" >&2 } error() { local ec="$1" shift echo "ERROR: $@" >&2 exit ${ec} } # Mount the downloaded image (IMG file) # # mount_image(imgfile, mntpnt) # mount_image() { [ $# -eq 2 ] || error ${EC_ARGS} "mount_image: invalid arguments: $@" local imgfile="$1" local mntpnt="$2" local vn [ -d "${mntpnt}" ] || mkdir "${mntpnt}" echo "Mounting image ${imgfile} to ${mntpnt} ..." vn=$(vnconfig -v -c vn ${imgfile}) || exit ${EC_VN} mount -r /dev/${vn}s2a ${mntpnt} && echo "DONE" || exit ${EC_MOUNT} } # Get the vn device name of the mounted image get_vn_devname() { [ $# -eq 1 ] || error ${EC_ARGS} "get_vn_devname: invalid arguments: $@" local mntpnt="$1" local dev=$(mount | fgrep "${mntpnt}" | cut -d' ' -f 1 | cut -d'/' -f 3) echo ${dev%s??} } # Umount the image # # umount_image(mntpnt) # umount_image() { [ $# -eq 1 ] || error ${EC_ARGS} "umount_image: invalid arguments: $@" local mntpnt="$1" local vn=$(get_vn_devname ${mntpnt}) echo -n "Umounting image from ${mntpnt} ... " umount ${mntpnt} && echo "DONE" || exit ${EC_UMOUNT} echo "Disable and unconfigure VN device ${vn} ... " vnconfig -v -u ${vn} && echo "DONE" || exit ${EC_VN} } # Backup the old kernel backup_kernel() { [ $# -eq 0 ] || error ${EC_ARGS} "backup_kernel: invalid arguments: $@" local kerndir="/boot/kernel" local oldkerndir="${kerndir}.old" [ -d "${oldkerndir}" ] && { rm -r ${oldkerndir} warn "Removed previously backed kernel: ${oldkerndir}" } echo "Backing up current kernel to ${oldkerndir} ..." mkdir -p ${oldkerndir} chflags noschg ${kerndir}/kernel objcopy --strip-debug ${kerndir}/kernel ${oldkerndir}/kernel for f in ${kerndir}/*.ko; do objcopy --strip-debug ${f} ${oldkerndir}/${f##*/} done [ -f "${kerndir}/initrd.img" ] && cp -p ${kerndir}/initrd.img ${oldkerndir} [ -f "${kerndir}/initrd.img.gz" ] && cp -p ${kerndir}/initrd.img.gz ${oldkerndir} echo "DONE" } # Backup the old world # # backup_world(backfile) # backup_world() { [ $# -eq 1 ] || error ${EC_ARGS} "backup_world: invalid arguments: $@" local backfile="$1" local backdir=$(dirname "${backfile}") [ -d "${backdir}" ] || mkdir ${backdir} [ -f "${backfile}" ] && { rm -f "${backfile}" warn "Removed previously backed world: ${backfile}" } echo "Backing up current world to ${backfile} ..." tar -czf "${backfile}" \ --options gzip:compression-level=1 \ -C / \ etc bin sbin lib libexec \ usr/bin usr/sbin usr/lib usr/libexec && echo "DONE" || exit ${EC_TAR} } # Install the new system (kernel and world, excluding /etc) install_system() { [ $# -eq 0 ] || error ${EC_ARGS} "install_system: invalid arguments: $@" local file file2 item path echo "Installing the new kernel and world ..." echo " => Backing up the files to protect from overriding ..." for file in ${FILES_IGNORE}; do if [ -f "${file}" ]; then file2="${file}.${BAK_SUF}" cp -pf "${file}" "${file2}" # NOTE: do NOT use "mv" echo " * ${file} " fi done echo " => Creating distribution directories ..." for item in \ root:/ \ var:/var \ usr:/usr \ include:/usr/include; do file=BSD.${item%:*}.dist path=${item#*:} echo " * mtree: ${path} ... " mtree -deUq -f ${MNT_DIR}/etc.hdd/mtree/${file} -p ${path} || exit ${EC_MTREE} done echo " => Installing kernel and world ..." for item in \ COPYRIGHT \ bin \ boot \ compat \ lib \ libexec \ rescue \ sbin \ usr/Makefile \ usr/bin \ usr/games \ usr/include \ usr/lib \ usr/libdata \ usr/libexec \ usr/sbin \ usr/share \ var/msgs \ var/yp; do echo -n " * Installing: ${item} ... " cpdup -o -u ${MNT_DIR}/${item} /${item} || exit ${EC_CPDUP} echo "ok" done echo " => Recovering the backed files ..." for file in ${FILES_IGNORE}; do file2="${file}.${BAK_SUF}" if [ -f "${file2}" ]; then mv -f "${file2}" "${file}" echo " * ${file} " fi done echo " => DONE!" } # Upgrade the system with new configuration files upgrade_system() { [ $# -eq 0 ] || error ${EC_ARGS} "upgrade_system: invalid arguments: $@" local etcdir="${CACHE_DIR}/etc.new" local file file_etc file_new [ -d "${CACHE_DIR}" ] || mkdir "${CACHE_DIR}" echo "Upgrading system ..." echo " => Coping new /etc to: ${etcdir}" cpdup -o -u ${MNT_DIR}/etc.hdd ${etcdir} || exit ${EC_CPDUP} echo " => Removing ignored files ..." for file_etc in ${FILES_IGNORE}; do file_new="${etcdir}/${file_etc#/etc/}" if [ -f "${file_new}" ]; then rm -f "${file_new}" echo " * ${file_new} " fi done echo " => Identifying new/updated config files ..." (cd "${etcdir}" && find -s . -type f) | while read -r file; do file_etc="/etc/${file#./}" file_new="${etcdir}/${file#./}" if [ -f "${file_etc}" ]; then if cmp -s "${file_etc}" "${file_new}"; then rm -f "${file_new}" else mv "${file_new}" "${file_new}.${NEW_SUF}" echo " * ${file_new} [UPDATED]" fi else echo " * ${file_new} [NEW]" fi done echo " => Installing new configurations ..." cpdup -o -u ${etcdir} /etc || exit ${EC_CPDUP} echo " => DONE!" rm -rf "${etcdir}" echo "+---------+" echo "| WARNING | Files with '${NEW_SUF}' suffix need manual merge!" echo "+---------+" } # Clean up obsolete and deprecated files cleanup() { [ $# -eq 0 ] || error ${EC_ARGS} "cleanup: invalid arguments: $@" local mk_upgrade tmpfile item itemcat mk_upgrade=/etc/upgrade/Makefile_upgrade.inc [ -e "${mk_upgrade}.${NEW_SUF}" ] && mk_upgrade=${mk_upgrade}.${NEW_SUF} tmpfile=$(mktemp -t ${NAME}) || exit ${EC_TMPFILE} echo "Removing obsolete and deprecated files ..." echo "(according to ${mk_upgrade})" make -f ${mk_upgrade} -V TO_REMOVE | tr ' ' '\n' > ${tmpfile} make -f ${mk_upgrade} -V TO_REMOVE_LATE | tr ' ' '\n' >> ${tmpfile} # Credit: https://stackoverflow.com/a/10929511 # [ -n "${item}" ]: do not ignore the last line if not end with a '\n' while IFS='' read -r item || [ -n "${item}" ]; do if [ -n "${item}" ] && [ -e ${item} -o -L ${item} ]; then echo " * ${item}" chflags -Rf noschg ${item} rm -rf ${item} fi if echo "${item}" | grep -q '/man/man[1-9]/'; then itemcat=$(echo "${item}" | sed 's|/man/man|/man/cat|') if [ -e "${itemcat}" ]; then echo " * ${itemcat}" chflags -Rf noschg ${itemcat} rm -rf ${itemcat} fi fi done < ${tmpfile} rm -f ${tmpfile} echo "DONE" } # Post-upgrade checking and report: # * check /etc for newly installed files that need manual merge postupgrade() { [ $# -eq 0 ] || error ${EC_ARGS} "postupgrade: invalid arguments: $@" echo "Rebuild capability database ..." cap_mkdb /etc/login.conf echo "Rebuild password database ..." pwd_mkdb -p /etc/master.passwd echo "Rebuild whatis database ..." makewhatis echo "Rebuild shared library cache ..." ldconfig -R echo "+=========================================================+" echo "The following config files need manual merge:" echo "+---------------------------------------------------------+" find -s /etc -name "*.${NEW_SUF}" | sort cat << _EOF_ +---------------------------------------------------------+ After manually merge the above files, reboot into the new system, and upgrade the packages with: # pkg upgrade -f +=========================================================+ _EOF_ } # # Sub-command functions # cmd_version() { cat <<_EOF_ v${VERSION} Aaron LI https://github.com/liweitianux/dfly-update _EOF_ } cmd_usage() { cat <<_EOF_ dfly-update - DragonFly BSD update tool using binary release/snapshots Usage: help | --help | -h Show this help. version | --version | -v Show version information of this tool. mount Mount the given image file backup Back up the current kernel and world upgrade Install the new kernel, world, and config files cleanup Clean up obsolete files, umount and remove image file fly | go Synthetic command to upgrade the system! _EOF_ echo cmd_version } # Mount the given image file # # usage: # cmd_mount cmd_mount() { [ $# -eq 1 ] || error ${EC_ARGS} "cmd_mount: invalid arguments: $@" local file="$1" [ -f "${file}" ] || error ${EC_NOFILE} "cmd_mount: file not exists: ${file}" mount_image "${file}" "${MNT_DIR}" } # Back up the current kernel and world cmd_backup() { [ $# -eq 0 ] || error ${EC_ARGS} "cmd_backup: invalid arguments: $@" backup_kernel backfile="${BACK_DIR}/world.tar.gz" backup_world "${backfile}" } # Install the new kernel, world, and config files. cmd_upgrade() { [ $# -eq 0 ] || error ${EC_ARGS} "cmd_upgrade: invalid arguments: $@" install_system upgrade_system } # Clean up obsolete files, umount and remove image cmd_cleanup() { [ $# -eq 0 ] || error ${EC_ARGS} "cmd_cleanup: invalid arguments: $@" cleanup umount_image ${MNT_DIR} postupgrade } # Integrate all the upgrading steps -> fly :-) cmd_fly() { [ $# -eq 1 ] || error ${EC_ARGS} "cmd_fly: invalid arguments: $@" cmd_mount "$1" cmd_backup cmd_upgrade cmd_cleanup } # # Main # # Load configurations [ -r "${CONFIGFILE}" ] && . ${CONFIGFILE} COMMAND="$1" case "${COMMAND}" in mount) shift cmd_mount "$@" ;; backup) shift cmd_backup ;; upgrade) shift cmd_upgrade ;; cleanup) shift cmd_cleanup ;; fly|go) shift cmd_fly "$@" ;; version|--version|-v) cmd_version ;; help|--help|-h|*) cmd_usage ;; esac exit 0