#!/usr/local/bin/cbsd
#v12.1.9
MYARG="mode"
MYOPTARG="arch default_obtain_etcupdate_method etcupdate_create_backup force from jname mode target_arch to ver"
MYDESC="etcupdate helper, manage updates to system files not updated by installworld"
CBSDMODULE="jail"
ADDHELP="
${H3_COLOR}Description${N0_COLOR}:

  To solve the distribution file synchronization problem (this is basically the
  contents of the /etc directory) when changing versions of the base system, 
  FreeBSD offers two utilities: mergemaster(8) and etcupdate(8). CBSD has a 
  script for working with etcupdate under the same name. By default CBSD 
  creates a directory hierarchy for working etcupdate in the system directory 
  of the database and each jail. You can turn this off by overriding the 
  'etcupdate_init' parameter value to '0' (disabled) in the profile of your 
  container or globally via 'jail-freebsd-default.conf' the configuration 
  file in the '~cbsd/etc/' directory.

${H3_COLOR}Options${N0_COLOR}:

 ${N2_COLOR}default_obtain_etcupdate_method=${N0_COLOR} - overwrite obtain 
   method, valid values: 'build','index'.
 ${N2_COLOR}etcupdate_create_backup=${N0_COLOR}         - (integer) when
   positive: number of etcupdate backup files, 0 - disable.
 ${N2_COLOR}force=1${N0_COLOR}                          - force to 
   extract/build even if resource already exist.
 ${N2_COLOR}from=${N0_COLOR}                            - from X.Y version.
 ${N2_COLOR}jname=${N0_COLOR}                           - target jail name.
 ${N2_COLOR}mode=${N0_COLOR}                             - can be:
   build extract purge resolve update:
   - mode=build   - (re)build index file for basejail.
   - mode=diff    - execute etcupdate diff between version.
   - mode=extract - when  jname is set, extract etc files for specified jail
   - mode=resolv  - run 'etcupdate resolv' to fix conflicts if they arise
 ${N2_COLOR}to=${N0_COLOR}                              - to X.Y version

${H3_COLOR}Examples${N0_COLOR}:

  Full update cycle between 12.2 and 13.0 version:

  1) create test jail on 12.2 version

    # cbsd jcreate jname=test ver=12.2

  2) create/build index files for 12.2 base if necessary ( only once for each base )

    # cbsd etcupdate mode=extract default_obtain_etcupdate_method=index ver=12.2
    # cbsd etcupdate mode=build default_obtain_etcupdate_method=index ver=12.2

  3) fetch and create/build (if necessary) new base version: 13.0

    # cbsd repo action=get sources=base ver=13.0
    # cbsd etcupdate mode=extract default_obtain_etcupdate_method=index ver=13.0
    # cbsd etcupdate mode=build default_obtain_etcupdate_method=index ver=13.0

  4) change jail version to new, 13.0:

    # cbsd jset jname=test ver=13.0
    # cbsd jstart test

  5) Merge/sync/update config files:

    # cbsd etcupdate jname=test mode=update from=12.2 to=13.0

${H3_COLOR}See also${N0_COLOR}:

 cbsd world --help
 cbsd etcupdate --help
 man 8 etcupdate

"
arch=
target_arch=
etcupdate_create_backup=

. ${subrdir}/nc.subr
. ${strings}
. ${subrdir}/universe.subr
readconf buildworld.conf

. ${cbsdinit}

[ -n "${target_arch}" ] && otarget_arch="${target_arch}"
[ -n "${arch}" ] && oarch="${arch}"

etcupdate_base_extract()
{
	local _etcupdate_dir _index_ver _index_file

	# todo: generate from ~cbsd/basejail/* ? gen from etcupdate required for source tree!
	[ -z "${etcupdate_ver}" ] && err 1 "${N1_COLOR}${CBSD_APP} error: no such ${N2_COLOR}ver=${N0_COLOR}"
	ver="${etcupdate_ver}"

	init_target_arch
	init_srcdir

	_etcupdate_dir="${srcdir}/src_${ver}/etcupdate"

	if [ -d ${_etcupdate_dir}/current ]; then
		if [ ${force} -eq 0 ]; then
			${ECHO} "${N1_COLOR}${CBSD_APP}: already exist: ${N2_COLOR}${_etcupdate_dir}/current${N0_COLOR}"
			return 0
		fi
		${RM_CMD} -rf ${_etcupdate_dir}/current
	fi

	case "${default_obtain_etcupdate_method}" in
		build)
			[ ! -r ${SRC_DIR}/Makefile ] && err 1 "${N1_COLOR}${CBSD_APP} error: no such src hier in ${SRC_DIR}. Please run: ${N2_COLOR}cbsd srcup ver=${ver}${N1_COLOR} first${N0_COLOR}"
			${ECHO} "${N1_COLOR}${CBSD_APP}: extract to: ${N2_COLOR}${_etcupdate_dir}/current${N0_COLOR}"
			${ETCUPDATE_CMD} extract -d ${_etcupdate_dir} -s ${SRC_DIR}
			[ ! -d ${_etcupdate_dir}/current ] && err 1 "${N1_COLOR}${CBSD_APP} failed to extract: ${N1_COLOR}${_etcupdate_dir}${N0_COLOR}"
			${ECHO} "${N1_COLOR}${CBSD_APP} updated: ${N2_COLOR}${_etcupdate_dir}${N0_COLOR}"
			;;
		index)
			_index_ver="${ver}"
			_index_file="${distsharedir}/etcupdate_${_index_ver}.txt.xz"
			if [ ! -r ${_index_file} ]; then
				# cut . in ver
				_index_ver=${ver%%.*}
				_index_file="${distsharedir}/etcupdate_${_index_ver}.txt.xz"
			fi
			[ ! -r "${_index_file}" ] && err 1 "${N1_COLOR}${CBSD_APP}: no index for default_obtain_etcupdate_method=index method: ${N2_COLOR}${_index_file}${N0_COLOR}"
			over="${ver}"
			. ${subrdir}/build.subr
			init_target_arch
			init_basedir

			if [ ! -x "${BASE_DIR}/bin/sh" ]; then
				${ECHO} "${N1_COLOR}${CBSD_APP}: no such base for default_obtain_etcupdate_method=index method: ${N2_COLOR}${BASE_DIR}${N0_COLOR}"
				err 1 "${N1_COLOR}${CBSD_APP}: please use: ${N2_COLOR}cbsd repo action=get sources=base ver=${ver}${N0_COLOR}"
			fi

			[ -d ${_etcupdate_dir}/current ] && ${RM_CMD} -rf ${_etcupdate_dir}/current
			${ECHO} "${N1_COLOR}${CBSD_APP}: extract to: ${N2_COLOR}${_etcupdate_dir}/current${N1_COLOR} (by: ${N2_COLOR}${_index_file} <- ${BASE_DIR}${N1_COLOR})${N0_COLOR}" 1>&2
			copy-binlib filelist=${_index_file} dstdir=${_etcupdate_dir}/current basedir=${BASE_DIR} 2>/dev/null
			;;
		*)
			err 1 "${N1_COLOR}${CBSD_APP}: unknown default_obtain_etcupdate_method: ${N2_COLOR}${default_obtain_etcupdate_method}${N0_COLOR}"
			;;
	esac

	return 0
}

etcupdate_jail_extract()
{
	local _etcupdate_dir _etcupdate_from_dir

	[ -z "${jail_ver}" ] && err 1 "${N1_COLOR}${CBSD_APP} error: no such ${N2_COLOR}ver=${N0_COLOR}"
	[ -z "${jname}" ] && log_err 1 "${N1_COLOR}Please set jset: ${N2_COLOR}jname${N0_COLOR}"

	_etcupdate_from_dir="${srcdir}/src_${ver}/etcupdate"
	if [ ! -d ${_etcupdate_from_dir}/current ]; then
		${ECHO} "${N1_COLOR}${CBSD_APP} error: no such ${N2_COLOR}${_etcupdate_from_dir}/current${N0_COLOR}"
		err 1 "${N1_COLOR}${CBSD_APP}: please run for init: ${N2_COLOR}cbsd etcupdate mode=extract ver=${ver}${N0_COLOR}"
	fi

	[ ! -d ${etcupdate_jail_root} ] && ${MKDIR_CMD} -p ${etcupdate_jail_root}

	if [ -d ${etcupdate_jail_root}/current ]; then
		[ ${force} -eq 0 ] && return 0
		${RM_CMD} -rf ${etcupdate_jail_root}/current
	fi

	[ ! -d ${etcupdate_jail_root}/current ] && ${CP_CMD} -a ${_etcupdate_from_dir}/current ${etcupdate_jail_root}

	# store original version
	[ ! -r ${etcupdate_jail_root}/ver ] && ${TOUCH_CMD} ${etcupdate_jail_root}/ver
	/usr/local/cbsd/misc/cbsdsysrc -qf ${etcupdate_jail_root}/ver etcupdate_current_ver="${ver}" > /dev/null 2>&1

	return 0
}

etcupdate_base_build()
{
	local _etcupdate_dir=

	# todo: generate from ~cbsd/basejail/* ? gen from etcupdate required for source tree!
	[ -z "${etcupdate_ver}" ] && err 1 "${N1_COLOR}${CBSD_APP} error: no such ${N2_COLOR}ver=${N0_COLOR}"
	ver="${etcupdate_ver}"

	init_target_arch
	init_srcdir

	_etcupdate_dir="${srcdir}/src_${ver}/etcupdate"

	if [ -r ${_etcupdate_dir}/etcupdate.tgz ]; then
		if [ ${force} -eq 0 ]; then
			${ECHO} "${N1_COLOR}${CBSD_APP}: already exist: ${N2_COLOR}${_etcupdate_dir}/etcupdate.tgz${N0_COLOR}"
			return 0
		fi
		${RM_CMD} -f ${_etcupdate_dir}/etcupdate.tgz
	fi

	case "${default_obtain_etcupdate_method}" in
		build)
			[ ! -r ${SRC_DIR}/Makefile ] && err 1 "${N1_COLOR}${CBSD_APP} error: no such src hier in ${SRC_DIR}. Please run: ${N2_COLOR}cbsd srcup ver=${ver}${N1_COLOR} first${N0_COLOR}"
			${ECHO} "${N1_COLOR}${CBSD_APP}: ${N2_COLOR}build...${N0_COLOR}"
			${ETCUPDATE_CMD} build -s ${SRC_DIR} ${_etcupdate_dir}/etcupdate.tgz
			if [ -r ${_etcupdate_dir}/etcupdate.tgz ]; then
				# done
			else
				err 1 "${N1_COLOR}${CBSD_APP} failed: no ${N2_COLOR}${_etcupdate_dir}/etcupdate.tgz${N0_COLOR}"
			fi
			;;
		index)
			[ ! -d ${_etcupdate_dir}/current ] && etcupdate mode=extract ver=${ver}
			[ ! -d ${_etcupdate_dir}/current ] && err 1 "${N1_COLOR}${CBSD_APP} no dir: ${N1_COLOR}${_etcupdate_dir}/current${N1_COLOR}. please run: ${N2_COLOR}cbsd etcupdate mode=extract ver=${ver}${N0_COLOR}"
			${ECHO} "${N1_COLOR}${CBSD_APP}: build to: ${N2_COLOR}${_etcupdate_dir}/etcupdate.tgz${N1_COLOR} (from: ${N2_COLOR}${_etcupdate_dir}/current${N1_COLOR})${N0_COLOR}"
			cd ${_etcupdate_dir}/current
			${TAR_CMD} cfz ${_etcupdate_dir}/etcupdate.tgz .
			[ ! -r ${_etcupdate_dir}/etcupdate.tgz ] && err 1 "${N1_COLOR}${CBSD_APP} failed to build: ${N1_COLOR}${_etcupdate_dir}/etcupdate.tgz${N0_COLOR}"
			;;
	esac

	return 0
}

etcupdate_update()
{
	local _exclude_args= i _ret _etcupdate_to_current_dir
	local _files _etcupdate_to_dir _etcupdate_from_dir
	local _to_remove _file_num _file_list

	[ -z "${jname}" ] && log_err 1 "${N1_COLOR}${CBSD_APP}: please set jset: ${N2_COLOR}jname${N0_COLOR}"

	if [ -z "${from}" ]; then
		if [ -r ${etcupdate_jail_root}/ver ]; then
			etcupdate_current_ver=
			. ${etcupdate_jail_root}/ver
			if [ -n "${etcupdate_current_ver}" ]; then
				from="${etcupdate_current_ver}"
				${ECHO} "${N1_COLOR}${CBSD_APP} ${N2_COLOR}from=${N1_COLOR} not specified, assume jail version: ${N2_COLOR}${from}${N0_COLOR}"
			fi
		fi
	fi
	[ -z "${from}" ] && err 1 "${N1_COLOR}${CBSD_APP} error: no such ${N2_COLOR}from=${N0_COLOR}"

	if [ -z "${to}" ]; then
		${ECHO} "${N1_COLOR}${CBSD_APP} ${N2_COLOR}to=${N1_COLOR} not specified, assume jail version: ${N2_COLOR}${jail_ver}${N0_COLOR}"
		to="${jail_ver}"
	fi

	ver="${from}"
	init_target_arch
	init_srcdir

	_etcupdate_from_dir="${srcdir}/src_${ver}/etcupdate"

	if [ ! -d ${_etcupdate_from_dir}/current ]; then
		${ECHO} "${N1_COLOR}${CBSD_APP} error: no such ${N2_COLOR}${_etcupdate_from_dir}/current${N0_COLOR}"
		err 1 "${N1_COLOR}${CBSD_APP}: please run for init: ${N2_COLOR}cbsd etcupdate mode=extract ver=${ver}${N0_COLOR}"
	fi
	${ECHO} "${N1_COLOR}${CBSD_APP} source hier: ${N2_COLOR}${_etcupdate_from_dir}/current${N0_COLOR}"

	ver="${to}"
	init_target_arch
	init_srcdir
	_etcupdate_to_dir="${srcdir}/src_${ver}/etcupdate"
	_etcupdate_to_current_dir="${srcdir}/src_${ver}/etcupdate/current"

	[ ! -d ${_etcupdate_to_current_dir} ] && err 1 "${N1_COLOR}${CBSD_APP} no dir: ${N1_COLOR}${_etcupdate_to_current_dir}${N1_COLOR}. please run: ${N2_COLOR}cbsd etcupdate mode=extract ver=${ver}${N0_COLOR}"

	if [ ! -r ${_etcupdate_to_dir}/etcupdate.tgz ]; then
		${ECHO} "${N1_COLOR}${CBSD_APP} failed: no ${N2_COLOR}${_etcupdate_to_dir}/etcupdate.tgz${N0_COLOR}"
		err 1 "${N1_COLOR}${CBSD_APP}: please run for init: ${N2_COLOR}cbsd etcupdate mode=build ver=${ver}${N0_COLOR}"

	fi
	${ECHO} "${N1_COLOR}${CBSD_APP} destination hier: ${N2_COLOR}${_etcupdate_to_dir}/etcupdate.tgz${N0_COLOR}"

	[ ! -d ${etcupdate_jail_root} ] && ${MKDIR_CMD} -p ${etcupdate_jail_root}
	# we need RW place for log? redefine log path and use nullfs in RO?
	[ ! -d ${etcupdate_jail_root}/current ] && ${CP_CMD} -a ${_etcupdate_from_dir}/current ${etcupdate_jail_root}

	for i in ${etcupdate_exclude_path}; do
		if [ -z "${_exclude_args}" ]; then
			_exclude_args="-I ${i}"
		else
			_exclude_args="${_exclude_args} -I ${i}"
		fi
	done

	# backup and rotate area
	if [ -n "${etcupdate_create_backup}" -a "${etcupdate_create_backup}" != "0" ]; then
		_etcupdate_from_dir="${srcdir}/src_${ver}/etcupdate"
		# create backup by index file
		_back_time=$( ${DATE_CMD} "+%Y%m%d%H%M%S" )
		_back_dir="${etcupdate_jail_root}/backup/${_back_time}"
		[ ! -d ${_back_dir} ] && ${MKDIR_CMD} -p ${_back_dir}
		# generate index file for copy-binlib
		_tmp_filelist=$( ${MKTEMP_CMD} )
		${FIND_CMD} ${_etcupdate_to_current_dir} -mindepth 1 -type f | while read line; do
			[ -z "${line}" ] && continue
			p2=${line##*${_etcupdate_to_current_dir}}
			[ -z "${p2}" ] && continue
			echo "${p2}" >> ${_tmp_filelist}
		done
		${XZ_CMD} ${_tmp_filelist}
		copy-binlib filelist=${_tmp_filelist}.xz dstdir=${_back_dir} basedir=${path} 2>/dev/null
		${RM_CMD} ${_tmp_filelist}.xz
		cd ${etcupdate_jail_root}/backup
		nice -n 19 ${IDLE_IONICE} ${TAR_CMD} cfz ${_back_time}.tgz ${_back_time}
		${ECHO} "${N1_COLOR}${CBSD_APP}: created backup: ${N2_COLOR}${_back_dir}.tgz${N0_COLOR}"
		# rotate
		_file_list=$( ${FIND_CMD} ${etcupdate_jail_root}/backup -mindepth 1 -maxdepth 1 -name \*.tgz -type f -exec ${BASENAME_CMD} {} \; | ${SORT_CMD} -n | ${XARGS_CMD} )
		_file_num=$( echo ${_file_list} | ${WC_CMD} -w | ${AWK_CMD} '{printf $1}' )

		if [ ${_file_num} -gt ${etcupdate_create_backup} ]; then
			_to_remove=$(( _file_num - etcupdate_create_backup ))
			for i in ${_file_list}; do
				${ECHO} "  ${N1_COLOR}${CBSD_APP}: prune old backup (max ${etcupdate_create_backup}): ${N2_COLOR}${i}${N0_COLOR}"
				${RM_CMD} ${etcupdate_jail_root}/backup/${i}
				_to_remove=$(( _to_remove - 1 ))
				[ ${_to_remove} -eq 0 ] && break
			done
		fi
	fi

	[ ${baserw} -eq 1 ] && path="${data}"		# baserw has a different root

	echo "[debug]: ${ETCUPDATE_CMD} -d ${etcupdate_jail_root} -t ${_etcupdate_to_dir}/etcupdate.tgz -D ${path} ${_exclude_args} ${extra_etcupdate_flags}" 1>&2
	/bin/sh <<EOF
${ETCUPDATE_CMD} -d ${etcupdate_jail_root} -t ${_etcupdate_to_dir}/etcupdate.tgz -D ${path} ${_exclude_args} ${extra_etcupdate_flags}
EOF
	_ret=$?

	if [ ${_ret} -ne 0 ]; then
		if [ -d ${etcupdate_jail_root}/conflicts ]; then
			_files=$( ${FIND_CMD} ${etcupdate_jail_root}/conflicts -mindepth 1 -type f | while read a; do
				p2=${a##*${etcupdate_jail_root}/conflicts}
				[ -z "${p2}" ] && continue
				echo "  * ${p2}"
			done )
			${ECHO} "${W1_COLOR}${CBSD_APP} warning${N1_COLOR}: confict files:${N0_COLOR}"
			echo "${_files}"
			${ECHO} "${N1_COLOR}${CBSD_APP} please run: ${N2_COLOR}cbsd etcupdate mode=resolve jname=${jname}${N1_COLOR} to run etcupdate resolve${N0_COLOR}"
			${ECHO} "${N1_COLOR}${CBSD_APP}: use ${N2_COLOR}EDITOR=${N1_COLOR} environment variable to use favorite editor${N0_COLOR}"
		fi
	fi

	return ${_ret}
}

etcupdate_diff()
{
	local _ret
	[ -z "${jname}" ] && log_err 1 "${N1_COLOR}Please set jset: ${N2_COLOR}jname${N0_COLOR}"

	_etcupdate_from_dir="${srcdir}/src_${ver}/etcupdate"
	if [ ! -d ${_etcupdate_from_dir}/current ]; then
		err 1 "${N1_COLOR}no such etcupdate hier, please run: ${N2_COLOR}cbsd etcupdate mode=extract ver=${ver}${N0_COLOR}"
	fi
	${ECHO} "${N1_COLOR}${CBSD_APP} source hier: ${N2_COLOR}${_etcupdate_from_dir}/current${N0_COLOR}"

	${ETCUPDATE_CMD} diff -d ${etcupdate_jail_root} -D ${path}
	_ret=$?
	return ${_ret}
}

etcupdate_resolve()
{
	local _ret
	[ -z "${jname}" ] && log_err 1 "${N1_COLOR}Please set jset: ${N2_COLOR}jname${N0_COLOR}"

	_etcupdate_from_dir="${srcdir}/src_${ver}/etcupdate"
	if [ ! -d ${_etcupdate_from_dir}/current ]; then
		err 1 "${N1_COLOR}no such etcupdate hier, please run: ${N2_COLOR}cbsd etcupdate mode=extract ver=${ver}${N0_COLOR}"
	fi
	${ECHO} "${N1_COLOR}${CBSD_APP} source hier: ${N2_COLOR}${_etcupdate_from_dir}/current${N0_COLOR}"

	${ETCUPDATE_CMD} resolve -d ${etcupdate_jail_root} -D ${path}
	_ret=$?
	return ${_ret}
}

## MAIN
if [ "${platform}" = "DragonFly" ]; then
	# DFLY does't have etcupdate yet
	exit 0
fi
odefault_obtain_etcupdate_method=
oforce=
oetcupdate_create_backup=

[ -n "${etcupdate_create_backup}" ] && oetcupdate_create_backup="${etcupdate_create_backup}"
[ -n "${default_obtain_etcupdate_method}" ] && odefault_obtain_etcupdate_method="${default_obtain_etcupdate_method}"
[ -n "${force}" ] && oforce="${force}"

readconf etcupdate.conf

[ -n "${odefault_obtain_etcupdate_method}" ] && default_obtain_etcupdate_method="${odefault_obtain_etcupdate_method}"
[ -n "${oetcupdate_create_backup}" ] && etcupdate_create_backup="${oetcupdate_create_backup}"
[ -n "${oforce}" ] && force="${oforce}"

# set defaults
[ -z "${force}" ] && force=0

etcupdate_ver="${ver}"

is_prepare_offline_jail=0

if [ -n "${jname}" ]; then
	. ${subrdir}/rcconf.subr
	[ $? -eq 1 ] && log_err 1 "${N1_COLOR}${CBSD_APP}: no such jail: ${N2_COLOR}${jname}${N0_COLOR}"
	[ "${emulator}" != "jail" ] && err 1 "${N1_COLOR}${CBSD_APP}: for 'jail' type emulator only: ${N2_COLOR}${emulator}${N0_COLOR}"

	[ ${baserw} -eq 1 ] && path=${data}

	if [ ${jid} -eq 0 ]; then
		. ${subrdir}/universe.subr
		. ${subrdir}/system.subr
		readconf buildworld.conf
		init_basedir
		prepare_offline_jail
		is_prepare_offline_jail=1
	fi

	etcupdate_jail_root="${jailsysdir}/${jname}/etcupdate"
fi

jail_ver="${ver}"

case "${mode}" in
	extract)
		if [ -n "${jname}" ]; then
			etcupdate_jail_extract
			ret=$?
		else
			etcupdate_base_extract
			ret=$?
		fi
		;;
	build)
		etcupdate_base_build
		ret=$?
		;;
	diff)
		etcupdate_diff
		ret=$?
		;;
	update)
		etcupdate_update
		ret=$?

		if [ ${ret} -eq 0 ]; then
			${ECHO} "${N1_COLOR}${CBSD_APP}: update successfull, bootstrap for: ${N1_COLOR}${ver}${N0_COLOR}"
			etcupdate jname=${jname} mode=extract default_obtain_etcupdate_method=index ver=${to} force=1
		else
			${ECHO} "${N1_COLOR}${CBSD_APP}: update failed${N0_COLOR}"
		fi
		;;
	purge)
		[ -d ${etcupdate_jail_root} ] && ${RM_CMD} -rf ${etcupdate_jail_root}
		;;
	resolve)
		etcupdate_resolve
		ret=$?
		if [ ${ret} -eq 0 ]; then
			${ECHO} "${N1_COLOR}${CBSD_APP}: update successfull, bootstrap for: ${N1_COLOR}${ver}${N0_COLOR}"
			etcupdate jname=${jname} mode=extract default_obtain_etcupdate_method=index ver=${to} force=1
		else
			${ECHO} "${N1_COLOR}${CBSD_APP}: update failed${N0_COLOR}"
		fi
		;;
	*)
		err 1 "${N1_COLOR}${CBSD_APP} error: unknown mode: ${N2_COLOR}${mode}${N0_COLOR}"
		;;
esac

if [ -n "${jname}" ]; then
	[ ${is_prepare_offline_jail} -eq 1 ] && jcleanup jname=${jname}
fi

exit ${ret}
