#!/usr/local/bin/cbsd
#v11.1.0
MYARG="mode"
MYOPTARG="display header jname snapfs snapname"
MYDESC="Jail snapshot management"
CBSDMODULE="bhyve,jail"
ADDHELP="
${H3_COLOR}Description${N0_COLOR}:

Сreates file snapshots of data on filesystems that support it.
Do not confuse with 'checkpoint'.

On ZFS-based filesystems, the 'jsnapshot' script mark/labels the snapshots 
with 'cbsdsnap' PROPERTY:

  zfs set cbsdsnap:jname=\${jname} \${snappath}
  zfs set cbsdsnap:snapname=\${snapname} \${snappath}

and ignores other snapshots that don't have 'cbsdsnap' attrubute.

${H3_COLOR}Options${N0_COLOR}:

 ${N2_COLOR}display=${N0_COLOR} - list by comma for column,
              default: 'snapname,jname,creation,refer';
 ${N2_COLOR}header=${N0_COLOR}  - don't print header;
 ${N2_COLOR}jname=${N0_COLOR}   - target environment;
 ${N2_COLOR}mode=${N0_COLOR}    - action, can be:
              clone               - clone snapshot;
              create              - create new snapshot;
              destroy             - destroy snapshot;
              destroyall          - remove all snapshot for current jail;
              destroyall_original - remove all original snapshot, e.g:
                                      ZPOOL@cbsd-original-<jname>, <CLOUD>@boot-<jname>, ...;
              list                - list of CBSD snapshots, jname= to limit by <jname> only;
              rollback            - rollback the snapshot;
 ${N2_COLOR}snapfs=${N0_COLOR}   - can be: 'rsync', force snapshot engine;
 ${N2_COLOR}snapname=${N0_COLOR} - can be: 'gettimeofday' or any arbitrary word;

${H3_COLOR}Examples${N0_COLOR}:

 # cbsd jsnapshot mode=create jname=jail1 snapname=gettimeofday
 # cbsd jsnapshot mode=list
 # cbsd jsnapshot mode=list jname=jail1
 # cbsd jsnapshot mode=rollback jname=jail1 snapname=20220319193339

"

. ${subrdir}/nc.subr
. ${strings}
. ${cbsdinit}
. ${subrdir}/time.subr

show_header()
{
	local _header="${BOLD}${myheader}${N0_COLOR}"
	[ ${header} -ne 0 ] && ${ECHO} ${_header}
}

init_zfs_mod_by_jname()
{
	mod=

	if [ -n "${jname}" ]; then

		. ${subrdir}/rcconf.subr

		case "${emulator}" in
			jail)
				mod="${ZPOOL}/${jname}"
				;;
			bhyve)
				# only dsk1.vhd at the moment
				_dsk="dsk1.vhd"
				readconf zfs.conf
				. ${subrdir}/zfs.subr
				if is_getzvol ${data}/${_dsk}; then
					mod="${is_zvol}"
				else
					err 1 "${N1_COLOR}bhyve zfs_snapshot_create: unable to find zvol for ${data}/${_dsk}${N0_COLOR}"
				fi
				;;
		esac
	fi
}

init_zfs_snappath_by_jname()
{
	snappath=
	local _dsk

	case "${emulator}" in
		jail)
			snappath="${ZPOOL}/${jname}@${snapname}"
			;;
		bhyve)
			# only dsk1.vhd at the moment
			_dsk="dsk1.vhd"
			readconf zfs.conf
			. ${subrdir}/zfs.subr

			if is_getzvol ${data}/${_dsk}; then
				snappath="${is_zvol}@${snapname}"
			else
				err 1 "${N1_COLOR}bhyve zfs_snapshot_create: unable to find zvol for ${data}/${_dsk}${N0_COLOR}"
			fi
			;;
	esac
}

check_jname()
{
	[ -z "${jname}" ] && err 1 "${N1_COLOR}Please set ${N2_COLOR}jname=${N0_COLOR}"
	. ${subrdir}/rcconf.subr
	if [ $? -eq 1 ]; then
		# jail can be in unregister state, check it
		JAILRCCONF="${jailrcconfdir}/rc.conf_${jname}"
		[ ! -f "${JAILRCCONF}" ] && err 1 "${N1_COLOR}No such jail: ${N2_COLOR}${jname}${N0_COLOR}"
		. ${JAILRCCONF}
		unregister=1
	fi
}

# -c <print count only>
zfs_snapshot_list()
{
	local _fs _p1 _jname _createtime _status _snaptime
	local _count_only _snapname _refer _count=0

	_count_only=0

	while getopts "c" opt; do
		case "${opt}" in
			c) _count_only="1" ;;
		esac
		shift $(($OPTIND - 1))
	done

	init_zfs_mod_by_jname

	[ ${_count_only} -eq 0 ] && show_header

	for _fs in $( ${ZFS_CMD} list -H -r -t snapshot -o name ${mod} 2>/dev/null ); do
		_status=
		_jname=$( ${ZFS_CMD} get -H -o value -s local cbsdsnap:jname "${_fs}" 2>/dev/null )

		[ -z "${_jname}" ] && continue		# not my snap
		[ -n "${jname}" -a "${jname}" != "${_jname}" ] && continue
		_snapname=$( ${ZFS_CMD} get -H -o value -s local cbsdsnap:snapname "${_fs}" 2>/dev/null )
		[ -z "${_snapname}" ] && continue

		if [ ${_count_only} -eq 1 ]; then
			_count=$(( _count + 1 ))
			continue
		fi

		#populate values for in output string
		for _i in ${mydisplay}; do
			case "${_i}" in
				snapname)
					_status="${_status}${_snapname} "
					;;
				jname)
					_status="${_status}${_jname} "
					;;
				creation)
					_createtime=$( ${ZFS_CMD} get -H -o value creation "${_fs}" 2>/dev/null )
					_snaptime=$( ${DATE_CMD} -j -f "%a %b %d %H:%M %Y" "${_createtime}" "+%Y-%m-%d__%H:%M" )
					_status="${_status}${_snaptime} "
					;;
				refer)
					_refer=$( ${ZFS_CMD} get -H -o value refer "${_fs}" 2>/dev/null )
					_status="${_status}${_refer} "
					;;
			esac
		done
		${ECHO} "${N0_COLOR}${_status}"
	done

	[ ${_count_only} -eq 1 ] && printf "${_count}"

	return 0
}

zfs_snapshot_destroyall()
{
	local _fs _jname _snapname _count

	init_zfs_mod_by_jname

	st_time=$( ${DATE_CMD} +%s )
	_count=0

	for _fs in $( ${ZFS_CMD} list -H -r -t snapshot -o name ${mod} 2>/dev/null ); do
		_jname=$( ${ZFS_CMD} get -H -o value -s local cbsdsnap:jname "${_fs}" 2>/dev/null )
		[ -z "${_jname}" ] && continue
		[ -n "${jname}" -a "${jname}" != "${_jname}" ] && continue
		_snapname=$( ${ZFS_CMD} get -H -o value -s local cbsdsnap:snapname "${_fs}" 2>/dev/null )
		[ -z "${_snapname}" ] && continue
		zfs_snapshot_destroy ${_fs} && ${ECHO} "${N1_COLOR}zfs snapshot for ${jname} destroyed: ${N2_COLOR}${_snapname}${N0_COLOR}"
		_count=$(( _count + 1 ))
	done

	end_time=$( ${DATE_CMD} +%s )
	diff_time=$(( end_time - st_time ))
	diff_time=$( displaytime ${diff_time} )
	${ECHO} "${N1_COLOR}${CBSD_APP}: destroyed ${N2_COLOR}${_count}${N1_COLOR} snapshots ${N2_COLOR}in ${diff_time}${N0_COLOR}"
}

zfs_snapshot_destroy_by_snapname()
{
	local _fs _jname _snapname _count

	[ -z "${snapname}" ] && err 1 "${N1_COLOR}Please set ${N2_COLOR}snapname=${N1_COLOR} or use ${N2_COLOR}mode=destroyall ${N1_COLOR}to destroy all snapshots for this jail${N0_COLOR}"
	init_zfs_mod_by_jname

	st_time=$( ${DATE_CMD} +%s )
	_count=0

	for _fs in $( ${ZFS_CMD} list -H -r -t snapshot -o name ${mod} 2>/dev/null ); do
		_jname=$( ${ZFS_CMD} get -H -o value -s local cbsdsnap:jname "${_fs}" 2>/dev/null )
		[ -z "${_jname}" ] && continue
		[ -n "${jname}" -a "${jname}" != "${_jname}" ] && continue
		_snapname=$( ${ZFS_CMD} get -H -o value -s local cbsdsnap:snapname "${_fs}" 2>/dev/null )
		[ -z "${_snapname}" -o "${_snapname}" != "${snapname}" ] && continue
		zfs_snapshot_destroy ${_fs} && ${ECHO} "${N1_COLOR}zfs snapshot for ${jname} destroyed: ${N2_COLOR}${_snapname}${N0_COLOR}"
		# if cycle is not end than something wrong with parsing of snapname
		_count=$(( _count + 1 ))
	done

	end_time=$( ${DATE_CMD} +%s )
	diff_time=$(( end_time - st_time ))
	diff_time=$( displaytime ${diff_time} )
	${ECHO} "${N1_COLOR}${CBSD_APP}: destroyed ${N2_COLOR}${_count}${N1_COLOR} snapshots ${N2_COLOR}in ${diff_time}${N0_COLOR}"
}

zfs_snapshot_destroyall_original()
{
	local _fs _jname _snapname ZPOOL
	local _parent _couunt

	readconf zfs.conf

	if [ "${emulator}" = "bhyve" ]; then
		#[ -z "${NOINTER}" ] && ${ECHO} "${N1_COLOR}destroyall_original not supported yet for vm${N0_COLOR}"
		exit 0
	fi

	ZPOOL=$( ${ZFS_CMD} get -Ho value name ${jaildatadir} )

	[ -z "${ZPOOL}" ] && err 1 "${N1_COLOR}Empty zpool for: ${N2_COLOR}${jaildatadir}${N0_COLOR}"
	mod="${ZPOOL}"

	st_time=$( ${DATE_CMD} +%s )
	_count=0

	for _fs in $( ${ZFS_CMD} list -H -r -t snapshot -o name ${mod} 2>/dev/null ); do
		_snapname=${_fs##*@}
		[ "${_snapname}" != "cbsd-original-${jname}" ] && continue
		zfs_snapshot_destroy ${_fs} && ${ECHO} "${N1_COLOR}zfs original snapshot for ${jname} destroyed: ${N2_COLOR}${_fs}${N0_COLOR}"
		_count=$(( _count + 1 ))
	done

	end_time=$( ${DATE_CMD} +%s )
	diff_time=$(( end_time - st_time ))
	diff_time=$( displaytime ${diff_time} )
	${ECHO} "${N1_COLOR}${CBSD_APP}: destroyed ${N2_COLOR}${_count}${N1_COLOR} snapshots ${N2_COLOR}in ${diff_time}${N0_COLOR}"
}

rsync_snapshot_list()
{
	err 1 "${N1_COLOR}jsnapshot: rsync not implemented${N0_COLOR}"
}

ufs_snapshot_list()
{
	err 1 "${N1_COLOR}jsnapshot: ufs not implemented${N0_COLOR}"
	fs_type="ufs"

	for snap in $( snapinfo ${fs_dir} 2>/dev/null ); do
		[ ! -f ${snap} ] && continue

		#   determine sizes
		fs_size=$( ${DF_CMD} -k ${fs_dir} | ${TAIL_CMD} -n1 | ${AWK_CMD} '{ print $2; }' )
		used_size=$( ${DF_CMD} -k ${fs_dir} | ${TAIL_CMD} -n1 | ${AWK_CMD} '{ print $3; }' )
		snap_size=$( ${DU_CMD} -k ${snap} | ${AWK_CMD} '{ print $1; }' )

		#   determine snapshot creation time
		if [ ".${verbose}" = .yes ]; then
			snap_time=$( ${STAT_CMD} -f "%B" $snap )
			snap_time=$( ${DATE_CMD} -r "${snap_time}" "+%Y-%m-%dT%H:%M" )
		fi

		#   calculate percentages
		snap_percent=` echo . | ${AWK_CMD} '{ printf("%.1f%%", (snap / fs) * 100); }' snap="${snap_size}" fs="${fs_size}" `
		used_percent=` echo . | ${AWK_CMD} '{ printf("%.1f%%", (used / fs) * 100); }' used="${used_size}" fs="${fs_size}" `

		#   canonicalize for output
		fs_size=$( canonksize ${fs_size} )
		snap_size=$( canonksize ${snap_size} )
		used_size=$( canonksize ${used_size} )
		snap_file=`echo "${snap}" | ${SED_CMD} -e 's;.*/\([^/]*\)$;\1;'`

		#   output snapshot information
		if [ ".${verbose}" = .yes ]; then
			printf "%-15s %4s %8s %7s %8s %7s  %-15s %s\n" "${fs_dir}" "${fs_type}" "${used_size}" "${used_percent}" "${snap_size}" "${snap_percent}" "${snap_file}" "${snap_time}"
		else
			printf "%-15s %8s %7s %8s %7s  %-15s\n" "$fs_dir" "${used_size}" "${used_percent}" "${snap_size}" "${snap_percent}" "${snap_file}"
		fi
	done
}

zfs_snapshot_create()
{
	local _tst _createtime
	local _zfs _dsk _ret _count

	[ -z "${snapname}" ] && err 1 "${N1_COLOR}Empty ${N2_COLOR}snapname=${N0_COLOR}"

	_createtime=$( ${DATE_CMD} "+%Y%m%d%H%M%S" )
	[ "${snapname}" = "gettimeofday" ] && snapname=${_createtime}

	init_zfs_snappath_by_jname

	st_time=$( ${DATE_CMD} +%s )

	_tst=$( ${ZFS_CMD} snapshot ${snappath} 2>&1 )
	_ret=$?
	[ ${_ret} -ne 0 ] && err 1 "${N1_COLOR}zfs snapshot error: ${N0_COLOR}${_tst}"

	_tst=$( ${ZFS_CMD} set cbsdsnap:jname=${jname} ${snappath} 2>&1 )
	_ret=$?
	[ ${_ret} -ne 0 ] && err 1 "${N1_COLOR}zfs snapshot error, unable to set cbsdsnap:jname=${jname}: ${N0_COLOR}${_tst}"

	_tst=$( ${ZFS_CMD} set cbsdsnap:snapname=${snapname} ${snappath} 2>&1 )
	_ret=$?
	[ ${_ret} -ne 0 ] && err 1 "${N1_COLOR}zfs snapshot error: ${N0_COLOR}${_tst}"

	_count=$( zfs_snapshot_list -c )

	end_time=$( ${DATE_CMD} +%s )
	diff_time=$(( end_time - st_time ))
	diff_time=$( displaytime ${diff_time} )
	${ECHO} "${N1_COLOR}${CBSD_APP}: created snapshot ${H3_COLOR}#${_count}${N1_COLOR} for ${jname} ${N2_COLOR}in ${diff_time}${N0_COLOR}"
}

ufs_snapshot_create()
{
	err 1 "${N1_COLOR}jsnapshot: ufs not implemented${N0_COLOR}"
}

rsync_snapshot_create()
{
	err 1 "${N1_COLOR}jsnapshot: rsync not implemented${N0_COLOR}"
}

zfs_snapshot_destroy()
{
	local _ret _tst

	[ -z "${1}" ] && return 1

	_tst=$( ${ZFS_CMD} destroy ${1} 2>&1 )
	_ret=$?
	[ ${_ret} -ne 0 ] && err 1 "${N1_COLOR}zfs snapshot destroy error: ${N0_COLOR}${_tst}"
}

ufs_snapshot_destroy()
{
	err 1 "${N1_COLOR}jsnapshot: ufs not implemented${N0_COLOR}"
}

ufs_snapshot_destroyall()
{
	err 1 "${N1_COLOR}jsnapshot: ufs not implemented${N0_COLOR}"
}

ufs_snapshot_destroy_by_snapname()
{
	err 1 "${N1_COLOR}jsnapshot: ufs not implemented${N0_COLOR}"
}

rsync_snapshot_destroy()
{
	err 1 "${N1_COLOR}jsnapshot: rsync not implemented${N0_COLOR}"
}

zfs_snapshot_rollback()
{
	local _createtime _snapname _jname _ret
	[ -z "${snapname}" ] && err 1 "${N1_COLOR}empty ${N2_COLOR}snapname=${N0_COLOR}"

	init_zfs_snappath_by_jname

	_jname=$( ${ZFS_CMD} get -H -o value -s local cbsdsnap:jname "${snappath}" 2>/dev/null)
	[ -z "${_jname}" ] && err 1 "${N1_COLOR}no such snapshot ${snapname} for ${jname}${N0_COLOR}"
	[ -n "${jname}" -a "${jname}" != "${_jname}" ] && err "${N1_COLOR}Found snapshot ${N2_COLOR}${snapname}${MANGETA} but its owner is ${N2_COLOR}${_jname}${N0_COLOR}"
	_snapname=$( ${ZFS_CMD} get -H -o value -s local cbsdsnap:snapname "${snappath}" )
	[ -z "${_snapname}" ] && err 1 "${N1_COLOR}Snapshot found but he is not created by ${N2_COLOR}cbsd${N0_COLOR} - skipp${N0_COLOR}"
	 _createtime=$( ${ZFS_CMD} get -H -o value creation "${snappath}" )

	_tst=$( ${ZFS_CMD} rollback -r ${snappath} 2>&1 )
	_ret=$?
	[ ${_ret} -ne 0 ] && err 1 "${N1_COLOR}zfs rollback error: ${N0_COLOR}${_tst}"

	${ECHO} "${N1_COLOR}Restored state to ${N2_COLOR}${snapname}${N1_COLOR} snapshot created in ${N2_COLOR}${_createtime}${N0_COLOR}"
}

ufs_snapshot_rollback()
{
	err 1 "${N1_COLOR}jsnapshot: ufs not implemented${N0_COLOR}"
}

rsync_snapshot_rollback()
{
	err 1 "${N1_COLOR}jsnapshot: rsync not implemented${N0_COLOR}"
}

#MAIN
if [ -z "${snapfs}" ]; then
	if [ "$zfsfeat" = "1" ]; then
		snapfs="zfs"
		. ${subrdir}/zfs.subr
		ZPOOL=$( ${ZFS_CMD} get -Ho value name ${jaildatadir} 2>/dev/null)
		[ -z "${ZPOOL}" ] && err 1 "${N1_COLOR}Can't find ZFS pool on ${N2_COLOR}${jaildatadir}${N0_COLOR}"
	else
		snapfs="ufs"
	fi
fi

[ -z "${display}" ] && display="jname,snapname,creation,refer"

#remove commas for loop action on header
mydisplay=$( echo ${display} | ${TR_CMD} ',' '  ' )

# upper for header
myheader=$( echo ${mydisplay} | ${TR_CMD} '[:lower:]' '[:upper:]' )

[ -z "${header}" ] && header=1

case "${mode}" in
	list)
		${snapfs}_snapshot_list | ${COLUMN_CMD} -t
		;;
	create)
		check_jname
		${snapfs}_snapshot_create
		;;
	destroy)
		check_jname
		${snapfs}_snapshot_destroy_by_snapname
		;;
	destroyall)
		check_jname
		${snapfs}_snapshot_destroyall
		;;
	destroyall_original)
		check_jname
		${snapfs}_snapshot_destroyall_original
		;;
	rollback)
		check_jname
		${snapfs}_snapshot_rollback
		;;
	*)
		err 1 "${N1_COLOR}Unknown mode: ${N2_COLOR}${mode}${N0_COLOR}"
esac

exit 0
