#!/bin/sh # # Copyright (c) 2017-2018 Aaron LI # MIT License # # Tool to update a DragonFly BSD system using binary releases or # snapshot builds. # set -e DEBUG=${DEBUG:-""} NAME="dfly-update" VERSION="0.?.?" 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 # # Default Configurations # # Base URL to Remote DragonFly BSD images URL_BASE="https://mirror-master.dragonflybsd.org" URL_MASTER="${URL_BASE}/snapshots/x86_64/images" URL_RELEASE="${URL_BASE}/iso-images" # Default to track the same branch as the installed system # * MASTER (i.e., the DEVELOPMENT branch) # * RELEASE # * (empty) - same as the local installed branch UPDATE_BRANCH= # 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}" # # Helper Functions # debug() { [ -n "${DEBUG}" ] && echo "DEBUG: $@" >&2 } warn() { echo "WARNING: $@" >&2 } error() { local ec="$1" shift echo "ERROR: $@" >&2 exit ${ec} } check_os() { [ $# -eq 0 ] || exit ${EC_ARGS} local os=$(uname -s) if [ "${os}" != "DragonFly" ]; then error ${EC_OS} "Not a DragonFly BSD system!" fi } # contains(string, substring) # # Returns 0 if the specified string contains the specified substring, # otherwise returns 1. # # Credit: https://stackoverflow.com/a/8811800 contains() { [ $# -eq 2 ] || exit ${EC_ARGS} local string="$1" local substring="$2" if [ "${string#*$substring}" != "$string" ]; then return 0 # $substring is in $string else return 1 # $substring is not in $string fi } # Determine the branch from the image filename get_branch_filename() { [ $# -eq 1 ] || exit ${EC_ARGS} if contains "$1" "-DEV-"; then echo "MASTER" else echo "RELEASE" fi } # Determine whether the given name refers to the master branch? is_master_branch() { [ $# -eq 1 ] || exit ${EC_ARGS} case "$1" in master|MASTER|[dD][eE][vV]*) return 0 ;; *) return 1 ;; esac } # Get the branch of the installed system # * DEVELOPMENT # * RELEASE get_local_branch() { [ $# -eq 0 ] || exit ${EC_ARGS} check_os uname -r | awk -F'-' '{ print $2 }' } # Get the version of local installed system get_local_version() { [ $# -eq 0 ] || exit ${EC_ARGS} check_os local version=$(uname -v | awk '{ print $2 }') echo "${version}" | awk -F'-' '{ print $1 }' | tr -d 'v' } # Get the URL of the MD5 list get_md5list_url() { [ $# -eq 1 ] || exit ${EC_ARGS} local branch="$1" if is_master_branch "${branch}"; then echo "${URL_MASTER}/CHECKSUM.MD5" else echo "${URL_RELEASE}/md5.txt" fi } # Determine the URL of the given image filename get_image_url() { [ $# -eq 1 ] || exit ${EC_ARGS} local filename="$1" local branch=$(get_branch_filename ${filename}) if is_master_branch "${branch}"; then echo "${URL_MASTER}/${filename}" else echo "${URL_RELEASE}/${filename}" fi } # Get the latest remote system image # Returns: # "_filename=''; _md5=''" get_latest_image() { [ $# -eq 1 ] || exit ${EC_ARGS} local branch="$1" local url_checksum=$(get_md5list_url ${branch}) local tmpchecksum=$(mktemp -t ${NAME}) || exit ${EC_TMPFILE} local latest_filename latest_md5 line echo "Fetch remote systems checksum: ${url_checksum}" >&2 fetch -q -o ${tmpchecksum} "${url_checksum}" || exit ${EC_FETCH} if is_master_branch "${branch}"; then line=$(fgrep '.img.bz2' ${tmpchecksum} | tail -n 1) else line=$(fgrep '.img.bz2' ${tmpchecksum} | \ fgrep -v 'gui-' | tail -n 1) fi latest_filename=$(echo "${line}" | awk -F'[()]' '{ print $2 }') latest_md5=$(echo "${line}" | awk '{ print $4 }') rm -f ${tmpchecksum} debug "_filename='${latest_filename}'; _md5='${latest_md5}'" echo "_filename='${latest_filename}'; _md5='${latest_md5}'" } # Extract the version from image filename get_version_filename() { [ $# -eq 2 ] || exit ${EC_ARGS} local branch="$1" local filename="$2" local version if is_master_branch "${branch}"; then version=$(echo "${filename}" | cut -d'-' -f5 | cut -d'.' -f1-5) else version=$(echo "${filename}" | cut -d'-' -f3 | cut -d'_' -f1) version=${version#v} fi echo ${version} } # Compare between two version strings # Parameters: ver1 ver2 # Returns values: # * 0 : ver1 = ver2 # * 1 : ver1 < ver2 # * 2 : ver1 > ver2 compare_version() { [ $# -eq 2 ] || exit ${EC_ARGS} local ver1="$1" local ver2="$2" local ver_low=$(echo -e "${ver1}\n${ver2}" | sort -V | head -n 1) if [ "${ver1}" = "${ver2}" ]; then echo 0 elif [ "${ver1}" = "${ver_low}" ]; then echo 1 else echo 2 fi } # Checksum the image file # # checksum_image(file, md5) # # Returns: # * 0 : file exists and its md5 hash matches the given one. # * 1 : otherwise checksum_image() { [ $# -eq 2 ] || exit ${EC_ARGS} local file="$1" local md5_match="$2" local md5 [ -f "${file}" ] && md5=$(md5 -q "${file}") if [ -n "${md5}" ] && [ "${md5}" = "${md5_match}" ]; then return 0 else return 1 fi } # Download the latest system image (IMG file) # # download_image(url, outfile) # download_image() { [ $# -eq 2 ] || exit ${EC_ARGS} local url="$1" local outfile="$2" local outdir=$(dirname "${outfile}") [ ! -d "${outdir}" ] && mkdir -v "${outdir}" echo "Downloading the new system image ..." echo " <= ${url}" echo " => ${outfile}" fetch -o "${outfile}" "${url}" \ && echo "DONE" \ || exit ${EC_FETCH} } # Mount the downloaded image (IMG file) # # mount_image(imgfile, mntpnt) # mount_image() { [ $# -eq 2 ] || exit ${EC_ARGS} local imgfile="$1" local mntpnt="$2" local vn=$(vnconfig -l | fgrep "not in use" | head -n 1 | cut -d':' -f 1) [ ! -d "${mntpnt}" ] && mkdir -v "${mntpnt}" echo "Mounting image ${imgfile} to ${mntpnt} ..." 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 ] || exit ${EC_ARGS} local mntpnt="$1" local dev=$(mount | fgrep "${mntpnt}" | cut -d' ' -f 1 | cut -d'/' -f 3) echo ${dev%s??} } # Get the filename configured for the vn device get_vn_filename() { [ $# -eq 1 ] || exit ${EC_ARGS} local vn="$1" echo $(vnconfig -l ${vn} | awk '{ print $3 }') } # Umount the image # # umount_image(mntpnt) # umount_image() { [ $# -eq 1 ] || exit ${EC_ARGS} 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 ] || exit ${EC_ARGS} local kerndir="/boot/kernel" local oldkerndir="${kerndir}.old" echo "Backing up current kernel to ${oldkerndir} ..." if [ -d "${oldkerndir}" ]; then warn "Previous backed up kernel already exists!" rm -r ${oldkerndir} warn "Removed previously backed up kernel: ${oldkerndir}" fi 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 "Backed up old kernel at: ${oldkerndir}" } # Backup the old world # # backup_world(backfile) # backup_world() { [ $# -eq 1 ] || exit ${EC_ARGS} local backfile="$1" local backdir=$(dirname "${backfile}") echo "Backing up current world to ${backfile} ..." [ ! -d "${backdir}" ] && mkdir -v ${backdir} if [ -f "${backfile}" ]; then warn " => Previously backed up world exists!" rm -f "${backfile}" warn " => Removed previously backed world: ${backfile}" fi 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} } } # # Sub-command functions # cmd_version() { [ $# -eq 0 ] || exit ${EC_ARGS} cat <<_EOF_ v${VERSION} Aaron LI https://github.com/liweitianux/dfly-update _EOF_ } cmd_usage() { [ $# -eq 0 ] || exit ${EC_ARGS} 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. status Show local installed system version and remote available version. download Download the given image and check aginst the given MD5 mount Mount the given image file backup Back up the current kernel and world _EOF_ echo cmd_version } cmd_status() { [ $# -eq 0 ] || exit ${EC_ARGS} local branch=$(get_local_branch) local version=$(get_local_version) local branch_remote version_remote has_update if [ -z "${UPDATE_BRANCH}" ]; then branch_remote=${branch} else branch_remote=${UPDATE_BRANCH} fi eval "$(get_latest_image ${branch_remote})" || exit $? version_remote=$(get_version_filename ${branch_remote} ${_filename}) cat <<_EOF_ Local installed system: branch: ${branch} version: ${version} Remote available system: branch: ${branch_remote} version: ${version_remote} filename: ${_filename} md5: ${_md5} _EOF_ has_update=$(compare_version ${version} ${version_remote}) if [ ${has_update} -eq 0 ]; then echo "^_^ Your DragonFly is up-to-date ^_^" elif [ ${has_update} -eq 1 ]; then echo "!!! Your DragonFly needs update !!!" else echo "??? Your DragonFly is newer than remote ???" fi } # Download the given image and check aginst the given MD5 # # usage: # cmd_download cmd_download() { [ $# -eq 2 ] || exit ${EC_ARGS} local filename="$1" local md5="$2" local url=$(get_image_url ${filename}) local filepath="${CACHE_DIR}/${filename}" download_image "${url}" "${filepath}" if checksum_image "${filepath}" "${md5}"; then echo "Downloaded and MD5-checked image file." else echo "Downloaded image file does not match the MD5!" exit ${EC_MD5} fi } # Mount the given image file # # usage: # cmd_mount cmd_mount() { [ $# -eq 1 ] || exit ${EC_ARGS} local filepath="$1" case "${filepath}" in *.bz2) echo -n "Decompressing file: ${filepath} ..." bunzip2 "${filepath}" echo "DONE" filepath="${filepath%.bz2}" ;; esac mount_image "${filepath}" "${MNT_DIR}" } # Back up the current kernel and world cmd_backup() { [ $# -eq 0 ] || exit ${EC_ARGS} backup_kernel backfile="${BACK_DIR}/world.tar.gz" backup_world "${backfile}" } # # Main # # Load configurations [ -r "${CONFIGFILE}" ] && . ${CONFIGFILE} COMMAND="$1" case "${COMMAND}" in version|--version|-v) shift cmd_version ;; status) shift cmd_status "$@" ;; download) shift cmd_download "$@" ;; mount) shift cmd_mount "$@" ;; backup) shift cmd_backup ;; help|--help|-h|*) cmd_usage ;; esac exit 0