#!/bin/sh # # SPDX-License-Identifier: BSD-3-Clause # # Copyright (c) 2018-2025, Christer Edwards # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * 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. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER 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. . /usr/local/share/bastille/common.sh usage() { # Build an independent usage for the export command # Valid compress/options for ZFS systems are raw, .gz, .tgz, .txz and .xz # Valid compress/options for non ZFS configured systems are .tgz and .txz # If no compression option specified, user must redirect standard output error_notify "Usage: bastille export [option(s)] TARGET PATH" cat << EOF Options: -a | --auto Auto mode. Start/stop jail(s) if required. --gz Export a ZFS jail using GZIP(.gz) compressed image. -r | --raw Export a ZFS jail to an uncompressed RAW image. -s | --safe Safely stop and start a ZFS jail before the exporting process. --tgz Export a jail using simple .tgz compressed archive instead. --txz Export a jail using simple .txz compressed archive instead. -v | --verbose Be more verbose during the ZFS send operation. --xz Export a ZFS jail using XZ(.xz) compressed image. -x | --debug Enable debug mode. Note: If no export option specified, the jail should be redirected to standard output. EOF exit 1 } zfs_enable_check() { # Temporarily disable ZFS so we can create a standard backup archive if checkyesno bastille_zfs_enable; then # shellcheck disable=SC2034 bastille_zfs_enable="NO" fi } opt_count() { COMP_OPTION=$((COMP_OPTION + 1)) } # Reset export options AUTO=0 GZIP_EXPORT= XZ_EXPORT= SAFE_EXPORT= USER_EXPORT= RAW_EXPORT= DIR_EXPORT= TXZ_EXPORT= TGZ_EXPORT= OPT_ZSEND="-R" COMP_OPTION="0" if [ -n "${bastille_export_options}" ]; then # Overrides the case options by the user defined option(s) automatically. # Add bastille_export_options="--optionA --optionB" to bastille.conf, or simply `export bastille_export_options="--optionA --optionB"` environment variable. # To restore the standard case options, empty bastille_export_options="" in bastille.conf, or `unset bastille_export_options` environment variable. # Reference "/bastille/issues/443" DEFAULT_EXPORT_OPTS="${bastille_export_options}" info "Default export option(s): '${DEFAULT_EXPORT_OPTS}'" # Don't shift here when default export options are explicitly denoted in the config file, hence TARGET will always be $1. for opt in ${DEFAULT_EXPORT_OPTS}; do case "${opt}" in -a|--auto) AUTO="1" ;; --gz) GZIP_EXPORT="1" opt_count ;; --xz) XZ_EXPORT="1" opt_count ;; --tgz) TGZ_EXPORT="1" opt_count zfs_enable_check ;; --txz) TXZ_EXPORT="1" opt_count zfs_enable_check ;; -s|--safe) SAFE_EXPORT="1" ;; -r|--raw) RAW_EXPORT="1" opt_count ;; -v|--verbose) OPT_ZSEND="-Rv" ;; -x) enable_debug ;; -*) error_notify "[ERROR]: Unknown Option: \"${1}\"" usage;; esac done else # Handle options while [ $# -gt 0 ]; do case "${1}" in -h|--help|help) usage ;; -a|--auto) AUTO=1 shift ;; --gz) GZIP_EXPORT="1" opt_count shift ;; --xz) XZ_EXPORT="1" opt_count shift ;; --tgz) TGZ_EXPORT="1" opt_count zfs_enable_check shift ;; --txz) TXZ_EXPORT="1" opt_count zfs_enable_check shift ;; -s|--safe) SAFE_EXPORT="1" shift ;; -r|--raw) RAW_EXPORT="1" opt_count shift ;; -v|--verbose) OPT_ZSEND="-Rv" shift ;; -x) enable_debug shift ;; -*) for _opt in $(echo ${1} | sed 's/-//g' | fold -w1); do case ${_opt} in a) AUTO=1 ;; x) enable_debug ;; *) error_exit "[ERROR]: Unknown Option: \"${1}\"" esac done shift ;; *) break ;; esac done fi if [ $# -gt 2 ] || [ $# -lt 1 ]; then usage fi TARGET="${1}" # Check for directory export if echo "${2}" | grep -q "\/"; then DIR_EXPORT="${2}" fi bastille_root_check set_target_single "${TARGET}" # Validate for combined options if [ "${COMP_OPTION}" -gt "1" ]; then error_exit "[ERROR]: Only one compression format can be used during export." fi if { [ -n "${TXZ_EXPORT}" ] || [ -n "${TGZ_EXPORT}" ]; } && [ -n "${SAFE_EXPORT}" ]; then error_exit "[ERROR]: Simple archive modes with safe ZFS export can't be used together." fi if ! checkyesno bastille_zfs_enable; then if [ -n "${XZ_EXPORT}" ] || [ -n "${GZIP_EXPORT}" ] || [ -n "${RAW_EXPORT}" ] || [ -n "${SAFE_EXPORT}" ] || [ "${OPT_ZSEND}" = "-Rv" ]; then error_exit "[ERROR]: Options --xz, --gz, --raw, --safe, and --verbose are valid for ZFS configured systems only." fi fi if [ -n "${SAFE_EXPORT}" ]; then # Check if container is running, otherwise just ignore if [ -z "$(/usr/sbin/jls name | awk "/^${TARGET}$/")" ]; then SAFE_EXPORT= fi fi # Export directory check if [ -n "${DIR_EXPORT}" ]; then if [ -d "${DIR_EXPORT}" ]; then # Set the user defined export directory bastille_backupsdir="${DIR_EXPORT}" else error_exit "[ERROR]: Path not found." fi fi # Fallback to default if missing config parameters if [ -z "${bastille_compress_xz_options}" ]; then bastille_compress_xz_options="-0 -v" fi if [ -z "${bastille_compress_gz_options}" ]; then bastille_compress_gz_options="-1 -v" fi create_zfs_snap() { # Take a recursive temporary snapshot if [ -z "${USER_EXPORT}" ]; then info "\nCreating temporary ZFS snapshot for export..." fi zfs snapshot -r "${bastille_zfs_zpool}/${bastille_zfs_prefix}/jails/${TARGET}@bastille_${TARGET}_${DATE}" } clean_zfs_snap() { # Cleanup the recursive temporary snapshot zfs destroy "${bastille_zfs_zpool}/${bastille_zfs_prefix}/jails/${TARGET}/root@bastille_${TARGET}_${DATE}" zfs destroy "${bastille_zfs_zpool}/${bastille_zfs_prefix}/jails/${TARGET}@bastille_${TARGET}_${DATE}" } export_check() { # Inform the user about the exporting method if [ -z "${USER_EXPORT}" ]; then if [ -n "$(/usr/sbin/jls name | awk "/^${TARGET}$/")" ]; then if [ -n "${SAFE_EXPORT}" ]; then EXPORT_AS="Safely exporting" else EXPORT_AS="Hot exporting" fi else EXPORT_AS="Exporting" fi if [ "${FILE_EXT}" = ".xz" ] || [ "${FILE_EXT}" = ".gz" ] || [ "${FILE_EXT}" = "" ]; then EXPORT_TYPE="image" else EXPORT_TYPE="archive" fi if [ -n "${RAW_EXPORT}" ]; then EXPORT_INFO="to a raw ${EXPORT_TYPE}" else EXPORT_INFO="to a compressed ${FILE_EXT} ${EXPORT_TYPE}" fi info "\n${EXPORT_AS} '${TARGET}' ${EXPORT_INFO}..." fi # Safely stop and snapshot the jail if [ -n "${SAFE_EXPORT}" ]; then bastille stop ${TARGET} create_zfs_snap bastille start ${TARGET} else create_zfs_snap fi if checkyesno bastille_zfs_enable; then if [ -z "${USER_EXPORT}" ]; then info "\nSending ZFS data stream..." fi fi } jail_export() { # Attempt to export the container DATE=$(date +%F-%H%M%S) if checkyesno bastille_zfs_enable; then if [ -n "${bastille_zfs_zpool}" ]; then if [ "$(zfs get -H -o value encryption ${bastille_zfs_zpool}/${bastille_zfs_prefix}/jails/${TARGET})" = "on" ]; then error_exit "[ERROR]: Exporting jails in encryoted datasets is not supported." fi if [ -n "${RAW_EXPORT}" ]; then FILE_EXT="" export_check # Export the raw container recursively and cleanup temporary snapshots if ! zfs send ${OPT_ZSEND} "${bastille_zfs_zpool}/${bastille_zfs_prefix}/jails/${TARGET}@bastille_${TARGET}_${DATE}" > "${bastille_backupsdir}/${TARGET}_${DATE}"; then clean_zfs_snap error_exit "[ERROR]: Failed to export jail: ${TARGET}" else clean_zfs_snap fi elif [ -n "${GZIP_EXPORT}" ]; then FILE_EXT=".gz" export_check # Export the raw container recursively and cleanup temporary snapshots if ! zfs send ${OPT_ZSEND} "${bastille_zfs_zpool}/${bastille_zfs_prefix}/jails/${TARGET}@bastille_${TARGET}_${DATE}" | gzip ${bastille_compress_gz_options} > "${bastille_backupsdir}/${TARGET}_${DATE}${FILE_EXT}"; then clean_zfs_snap error_exit "[ERROR]: Failed to export jail: ${TARGET}" else clean_zfs_snap fi elif [ -n "${XZ_EXPORT}" ]; then FILE_EXT=".xz" export_check # Export the container recursively and cleanup temporary snapshots if ! zfs send ${OPT_ZSEND} "${bastille_zfs_zpool}/${bastille_zfs_prefix}/jails/${TARGET}@bastille_${TARGET}_${DATE}" | xz ${bastille_compress_xz_options} > "${bastille_backupsdir}/${TARGET}_${DATE}${FILE_EXT}"; then clean_zfs_snap error_exit "[ERROR]: Failed to export jail: ${TARGET}" else clean_zfs_snap fi else FILE_EXT="" USER_EXPORT="1" export_check # Quietly export the container recursively, user must redirect standard output if ! zfs send ${OPT_ZSEND} "${bastille_zfs_zpool}/${bastille_zfs_prefix}/jails/${TARGET}@bastille_${TARGET}_${DATE}"; then clean_zfs_snap error_exit "[ERROR]: An export option is required, see 'bastille export, otherwise the user must redirect to standard output." else clean_zfs_snap fi fi fi else if [ -n "${TGZ_EXPORT}" ]; then FILE_EXT=".tgz" # Create standard tgz backup archive info "\nExporting '${TARGET}' to a compressed ${FILE_EXT} archive..." cd "${bastille_jailsdir}" || error_exit "[ERROR]: Failed to change to directory: ${bastille_jailsdir}" if ! tar -cf - "${TARGET}" | gzip ${bastille_compress_gz_options} > "${bastille_backupsdir}/${TARGET}_${DATE}${FILE_EXT}"; then error_exit "[ERROR]: Failed to export jail: ${TARGET}" fi elif [ -n "${TXZ_EXPORT}" ]; then FILE_EXT=".txz" # Create standard txz backup archive info "\nExporting '${TARGET}' to a compressed ${FILE_EXT} archive..." cd "${bastille_jailsdir}" || error_exit "[ERROR]: Failed to change to directory: ${bastille_jailssdir}" if ! tar -cf - "${TARGET}" | xz ${bastille_compress_xz_options} > "${bastille_backupsdir}/${TARGET}_${DATE}${FILE_EXT}"; then error_exit "[ERROR]: Failed to export jail: ${TARGET}" fi else error_exit "[ERROR]: export option required" fi fi # shellcheck disable=SC2181 if [ "$?" -ne 0 ]; then error_exit "[ERROR]: Failed to export jail: ${TARGET}" else if [ -z "${USER_EXPORT}" ]; then # Generate container checksum file cd "${bastille_backupsdir}" || error_exit "[ERROR]: Failed to change to directory: ${bastille_backupsdir}" if ! sha256 -q "${TARGET}_${DATE}${FILE_EXT}" > "${TARGET}_${DATE}.sha256"; then error_exit "[ERROR]: Failed to generate sha256 file." fi info "\nExported '${bastille_backupsdir}/${TARGET}_${DATE}${FILE_EXT}' successfully." fi exit 0 fi } # Check if backups directory/dataset exist if [ ! -d "${bastille_backupsdir}" ]; then error_exit "[ERROR]: Backups directory/dataset does not exist. See 'bastille bootstrap'." fi if [ -n "${TARGET}" ]; then # Validate jail existence if [ ! -d "${bastille_jailsdir}/${TARGET}" ]; then error_exit "[ERROR]: Jail not found: ${TARGET}" fi # Jail needs to be stopped on non-ZFS systems if ! checkyesno bastille_zfs_enable; then # Validate jail state check_target_is_stopped "${TARGET}" || if [ "${AUTO}" -eq 1 ]; then bastille stop "${TARGET}" else info "\n[${TARGET}]:" error_notify "Jail is running." error_exit "Use [-a|--auto] to auto-stop the jail." fi fi jail_export fi